Homebrew manager completed !

This commit is contained in:
Clément Pons
2025-08-26 15:27:47 +02:00
parent 80a94bee86
commit da93fcd82d
14 changed files with 1173 additions and 244 deletions

View File

@@ -745,8 +745,9 @@
}
]
},
"peoples": [
{
"peoples": {
"e662m19q590kn4dowvssowi1qf8ia7sk": {
"id": "e662m19q590kn4dowvssowi1qf8ia7sk",
"name": "Humain",
"description": "Les humains, originaire d'un tout autre monde, ont subit un cataclysme qui les a projeté dans les terres d'Erina. En tant que civilisation dépourvue de magie, ils sont plus specialisés, gagnant moins de statistiques mais pouvant plus tôt ou plus fréquemment obtenir certains bonus.",
"options": {
@@ -833,8 +834,75 @@
"9q8mf0u06oxxwqltyv58kbavs7qtoouw"
]
}
},
"3v3rwsn9bimpyd2fc95ml8wdrrmfsqb0": {
"id": "3v3rwsn9bimpyd2fc95ml8wdrrmfsqb0",
"name": "Quplothien",
"description": "Quploth est la région du monde abritant le plus de marchands et charlatans. Dû à la sur-désertification de leurs terres, ils ont appris à vivre en troquant les richesses. Leurs cités, denses et prospères, sont peu nombreuses et suscitent un tourisme culturel croissant.",
"options": {
"1": [
"bbuzw6awn2mkb05imdoecxfkc5zuj98i"
],
"2": [
"ub2ws6q8xdbngeouip02umvw2oox9r68"
],
"3": [
"5d7u2jvi4u0nnrzesderha3uo8kb3zjq"
],
"4": [
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3"
],
"5": [
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w"
],
"6": [
"dx5khvrhwkhhn8fv4b8pecuh8i5wtwij"
],
"7": [
"pfzopr4oyrsgxg0cbva16zzzly3kke9z"
],
"8": [
"fk0wmg94tlq78khq8zot2o5u4nnxr2gb"
],
"9": [
"u9vv3z280jgzab7pjwe9kexqjlpoxvax"
],
"10": [
"fuxn9ndabr5yl0rrdtilldssmjxso24p"
],
"11": [
"7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q"
],
"12": [
"dwvjqspm8l0gnks6y7u9vty0563u20kd"
],
"13": [
"bmh55yfypfw8rezd16m2cuocrx0kkfl4"
],
"14": [
"hatuas1yl3armteqwjwm1gjpsjp3v97x"
],
"15": [
"0fg543b25uppvollu9oxtowyxjjw1x5a"
],
"16": [
"l770gvirwzfbtfgfl29dxhvdh95f1m71"
],
"17": [
"m1zkviiz3ow1g7rwpkyygmyggphvoz8b"
],
"18": [
"uuc8vci5bk5kkx23a7ks1gu778fmu9w1"
],
"19": [
"fd076hnyjagipaez166p9xp3wtlf5sgw"
],
"20": [
"qcp28eysi3l3n438v41kowisdpq4ht61"
]
}
}
],
},
"training": {
"strength": {
"0": [
@@ -9452,6 +9520,106 @@
}
],
"id": "9q8mf0u06oxxwqltyv58kbavs7qtoouw"
},
"bbuzw6awn2mkb05imdoecxfkc5zuj98i": {
"id": "bbuzw6awn2mkb05imdoecxfkc5zuj98i",
"description": "Bonjour",
"effect": []
},
"ub2ws6q8xdbngeouip02umvw2oox9r68": {
"id": "ub2ws6q8xdbngeouip02umvw2oox9r68",
"description": "je",
"effect": []
},
"5d7u2jvi4u0nnrzesderha3uo8kb3zjq": {
"id": "5d7u2jvi4u0nnrzesderha3uo8kb3zjq",
"description": "suis",
"effect": []
},
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3": {
"id": "8w4jthjrn3l8u4trmj46z6t6ab5rbgk3",
"description": "Nicolas",
"effect": []
},
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w": {
"id": "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w",
"description": "Sarkozy",
"effect": []
},
"dx5khvrhwkhhn8fv4b8pecuh8i5wtwij": {
"id": "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij",
"description": "",
"effect": []
},
"pfzopr4oyrsgxg0cbva16zzzly3kke9z": {
"id": "pfzopr4oyrsgxg0cbva16zzzly3kke9z",
"description": "",
"effect": []
},
"fk0wmg94tlq78khq8zot2o5u4nnxr2gb": {
"id": "fk0wmg94tlq78khq8zot2o5u4nnxr2gb",
"description": "",
"effect": []
},
"u9vv3z280jgzab7pjwe9kexqjlpoxvax": {
"id": "u9vv3z280jgzab7pjwe9kexqjlpoxvax",
"description": "",
"effect": []
},
"fuxn9ndabr5yl0rrdtilldssmjxso24p": {
"id": "fuxn9ndabr5yl0rrdtilldssmjxso24p",
"description": "",
"effect": []
},
"7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q": {
"id": "7kdxs6b6j9pqhgm3c8ydp8f9o074vp8q",
"description": "",
"effect": []
},
"dwvjqspm8l0gnks6y7u9vty0563u20kd": {
"id": "dwvjqspm8l0gnks6y7u9vty0563u20kd",
"description": "",
"effect": []
},
"bmh55yfypfw8rezd16m2cuocrx0kkfl4": {
"id": "bmh55yfypfw8rezd16m2cuocrx0kkfl4",
"description": "",
"effect": []
},
"hatuas1yl3armteqwjwm1gjpsjp3v97x": {
"id": "hatuas1yl3armteqwjwm1gjpsjp3v97x",
"description": "",
"effect": []
},
"0fg543b25uppvollu9oxtowyxjjw1x5a": {
"id": "0fg543b25uppvollu9oxtowyxjjw1x5a",
"description": "",
"effect": []
},
"l770gvirwzfbtfgfl29dxhvdh95f1m71": {
"id": "l770gvirwzfbtfgfl29dxhvdh95f1m71",
"description": "",
"effect": []
},
"m1zkviiz3ow1g7rwpkyygmyggphvoz8b": {
"id": "m1zkviiz3ow1g7rwpkyygmyggphvoz8b",
"description": "",
"effect": []
},
"uuc8vci5bk5kkx23a7ks1gu778fmu9w1": {
"id": "uuc8vci5bk5kkx23a7ks1gu778fmu9w1",
"description": "",
"effect": []
},
"fd076hnyjagipaez166p9xp3wtlf5sgw": {
"id": "fd076hnyjagipaez166p9xp3wtlf5sgw",
"description": "",
"effect": []
},
"qcp28eysi3l3n438v41kowisdpq4ht61": {
"id": "qcp28eysi3l3n438v41kowisdpq4ht61",
"description": "",
"effect": []
}
}
}

View File

@@ -428,7 +428,7 @@ export class CharacterBuilder extends CharacterCompiler
}
private render()
{
/*this._steps = [
this._steps = [
PeoplePicker,
LevelPicker,
TrainingPicker,
@@ -439,7 +439,7 @@ export class CharacterBuilder extends CharacterCompiler
dom("div", { class: "group flex items-center", }, [
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]),
])
);*/
);
this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.")
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
@@ -460,15 +460,15 @@ export class CharacterBuilder extends CharacterCompiler
if(step < 0 || step >= this._stepsHeader.length)
return;
//if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
// return;
if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
return;
//this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
//this._stepsHeader[step]!.setAttribute('data-state', 'active');
this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
this._stepsHeader[step]!.setAttribute('data-state', 'active');
//this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);
this._content?.replaceChildren(...(new this._steps[step]!(this)).dom);
//this._helperText.textContent = this._steps[step]!.description;
this._helperText.textContent = this._steps[step]!.description;
}
async save(leave: boolean = true)
{
@@ -681,7 +681,7 @@ abstract class BuilderTab {
};
type BuilderTabConstructor = {
new (builder: CharacterBuilder): BuilderTab;
name: string;
header: string;
description: string;
validate(builder: CharacterBuilder): boolean;
}
@@ -691,8 +691,6 @@ class PeoplePicker extends BuilderTab
private _visibilityInput: HTMLDivElement;
private _options: HTMLDivElement[];
private _activeOption?: HTMLDivElement;
static override header = 'Peuple';
static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.';
@@ -704,16 +702,15 @@ class PeoplePicker extends BuilderTab
input: (value) => {
this._builder.character.name = value ?? '';
document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`;
}
}, defaultValue: this._builder.character.name
});
this._visibilityInput = toggle({ defaultValue: this._builder.character.visibility === "private", change: (value) => this._builder.character.visibility = value ? "private" : "public" });
this._options = config.peoples.map(
this._options = Object.values(config.peoples).map(
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
this._builder.character.people = i;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
this._activeOption = this._options[i]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
this._builder.character.people = people.id;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.forEach(f => f?.classList.toggle(e, false)));
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options[i]?.classList.toggle(e, true));
}
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
);
@@ -734,13 +731,6 @@ class PeoplePicker extends BuilderTab
{
this._nameInput.value = this._builder.character.name;
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
if(this._builder.character.people !== undefined)
{
this._activeOption = this._options[this._builder.character.people]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
}
}
static override validate(builder: CharacterBuilder): boolean
{

View File

@@ -47,10 +47,10 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
if(e === undefined)
return;
return dom('div', { listeners: { click: () => {
return dom('div', { listeners: { click: (_e) => {
textValue.textContent = e.text;
settings?.change && settings?.change(e.value);
context && context.close && context.close();
context && context.close && !_e.ctrlKey && context.close();
}, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
});
const select = dom('div', { listeners: { click: () => {
@@ -116,12 +116,12 @@ export function multiselect<T extends NonNullable<any>>(options: Array<{ text: s
if(e === undefined)
return;
const element = dom('div', { listeners: { click: () => {
const element = dom('div', { listeners: { click: (_e) => {
selection = selection.includes(e.value) ? selection.filter(f => f !== e.value) : [...selection, e.value];
textValue.textContent = selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '';
element.toggleAttribute('data-selected', selection.includes(e.value));
settings?.change && settings?.change(selection);
context && context.close && context.close();
context && context.close && !_e.ctrlKey && context.close();
}, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': selection.includes(e.value) } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block', noobserver: true }) ]);
return element;
});
@@ -240,11 +240,11 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
}
else
{
return { item: option, dom: dom('div', { listeners: { click: () => {
return { item: option, dom: dom('div', { listeners: { click: (_e) => {
select.value = option.text;
settings?.change && settings?.change(option.value as T);
selected = true;
hide();
!_e.ctrlKey && hide();
}, mouseenter: () => focus(option.value) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ option?.render ? option?.render() : text(option.text) ]) };
}
}
@@ -347,10 +347,10 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
switch(e.key)
{
case "ArrowUp":
validateAndChange(storedValue + (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
validateAndChange(storedValue + (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
break;
case "ArrowDown":
validateAndChange(storedValue - (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
validateAndChange(storedValue - (e.ctrlKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
break;
case "PageUp":
settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue);
@@ -371,12 +371,23 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
return field;
}
// Open by default
export function foldable(content: NodeChildren, title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
export function foldable(content: NodeChildren | (() => NodeChildren), title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
{
let _content: NodeChildren;
const display = (state: boolean) => {
if(state && !_content)
{
_content = typeof content === 'function' ? content() : content;
//@ts-ignore
contentContainer.replaceChildren(..._content);
}
}
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
const fold = div(['group flex flex-1 w-full flex-col', settings?.class?.container], [
div('flex', [ dom('div', { listeners: { click: () => fold.toggleAttribute('data-active') }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center' }) ]), div(['flex-1', settings?.class?.title], title) ]),
div(['hidden group-data-[active]:flex', settings?.class?.content], content),
div('flex', [ dom('div', { listeners: { click: () => { display(fold.toggleAttribute('data-active')) } }, class: ['flex justify-center items-center', settings?.class?.icon] }, [ icon('radix-icons:caret-right', { class: 'group-data-[active]:rotate-90 origin-center', noobserver: true }) ]), div(['flex-1', settings?.class?.title], title) ]),
contentContainer
]);
display(settings?.open ?? true);
fold.toggleAttribute('data-active', settings?.open ?? true);
return fold;
}

View File

@@ -1,10 +1,10 @@
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util";
import { MarkdownEditor } from "#shared/editor.util";
import { fakeA } from "#shared/proses";
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
import { ALIGNMENTS, alignmentTexts, elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
import { ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
import characterConfig from "#shared/character-config.json";
import { getID, ID_SIZE } from "#shared/general.util";
import renderMarkdown, { renderText } from "#shared/markdown.util";
@@ -100,24 +100,73 @@ abstract class BuilderTab {
};
class PeopleEditor extends BuilderTab
{
private _options: HTMLDivElement[];
private _activeOption?: HTMLDivElement;
constructor(builder: HomebrewBuilder, config: CharacterConfig)
{
super(builder, config);
this._options = config.peoples.map(
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
this._activeOption = this._options[i]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
const add = () => {
const people: RaceConfig = {
id: getID(ID_SIZE),
name: '',
description: '',
options: LEVELS.map(e => {
const feature: Feature = {
id: getID(ID_SIZE),
description: '',
effect: [],
}
config.features[feature.id] = feature;
return [e, [feature.id]] as [Level, string[]];
}).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<Level, string[]>)
};
config.peoples[people.id] = people;
(this._content[0] as HTMLDivElement).appendChild(peopleRender(people));
}
const remove = (people: RaceConfig) => {
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
if(e)
{
Object.values(people.options).forEach(e => e.forEach(id => delete config.features[id]));
delete config.peoples[people.id];
}
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
);
this._content = [ div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options) ];
})
}
const render = (people: string, level: Level, feature: string) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[feature]!).then(e => {
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
});
}, contextmenu: (e) => {
e.preventDefault();
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
config.features[_feature.id] = _feature;
config.peoples[people]!.options[level]!.push(_feature.id);
element.parentElement?.appendChild(render(people, level, _feature.id));
} } }, [ text('Nouveau') ]),
config.peoples[people]!.options[level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) {
config.peoples[people]!.options[level] = config.peoples[people]!.options[level].filter(e => e !== feature);
delete config.features[feature];
element.remove();
}
}) } } }, [ text('Supprimer') ]) : undefined,
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
}
const peopleRender = (people: RaceConfig) => {
return foldable(() => Object.entries(people.options).flatMap(level => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
div("flex flex-row gap-4 justify-center", level[1].map((option) => render(people.id, parseInt(level[0], 10) as Level, option))),
]), [ input('text', { defaultValue: people.name, input: (value) => people.name = value, class: 'w-32' }), input('text', { defaultValue: people.description, input: (value) => people.description = value, class: 'w-full' }) ], { class: { container: 'gap-2 max-h-full', title: 'flex flex-row', content: 'flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8' }, open: false })
}
const container = div('flex flex-col gap-2', Object.values(config.peoples).map(peopleRender));
this._content = [ div('flex flex-col py-2 gap-2', [ div('w-full flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), container ]) ];
}
}
class TrainingEditor extends BuilderTab
@@ -131,24 +180,37 @@ class TrainingEditor extends BuilderTab
constructor(builder: HomebrewBuilder, config: CharacterConfig)
{
super(builder, config);
const render = (stat: MainStat, level: TrainingLevel, feature: string) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[feature]!).then(e => {
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
});
}, contextmenu: (e) => {
e.preventDefault();
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
config.features[_feature.id] = _feature;
config.training[stat][level].push(_feature.id);
element.parentElement?.appendChild(render(stat, level, _feature.id));
} } }, [ text('Nouveau') ]),
config.training[stat][level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) {
config.training[stat][level as any as TrainingLevel] = config.training[stat][level as any as TrainingLevel].filter(e => e !== feature);
delete config.features[feature];
element.remove();
}
}) } } }, [ text('Supprimer') ]) : undefined,
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
};
const statRenderBlock = (stat: MainStat) => {
return Object.entries(config.training[stat]).map(
(level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[option]!).then(e => {
element.replaceChildren(markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }));
});
}, contextmenu: (e) => {
e.preventDefault();
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau avant') ]),
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); } } }, [ text('Nouveau après') ]),
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { delete config.training[stat][level[0] as any as TrainingLevel]; /* redraw */ } }) } } }, [ text('Supprimer') ])
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
})),
div("flex flex-row gap-4 justify-center", level[1].map((option) => render(stat, parseInt(level[0], 10) as TrainingLevel, option))),
])
}
@@ -229,11 +291,16 @@ class AspectEditor extends BuilderTab
content = element;
};
const remove = (aspect: AspectConfig) => {
config.aspects = config.aspects.filter(e => e !== aspect);
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
if(e)
{
config.aspects = config.aspects.filter(e => e !== aspect);
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
}
})
}
const redraw = () => table(config.aspects.map(render), { name: 'Nom', description: 'Description', stat: 'Buff de stat', alignment: 'Alignement', magic: 'Magie', difficulty: 'Difficulté', physic: 'Physique', mental: 'Mental', personality: 'Caractère', action: 'Actions' }, { class: { table: 'flex-1' } });
let content = redraw();
@@ -247,19 +314,15 @@ class SpellEditor extends BuilderTab
super(builder, config);
const render = (spell: SpellConfig) => {
return {
id: spell.id,
name: input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-full' }),
rank: select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 w-full' } }),
type: select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 w-full' } }),
cost: numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }),
speed: select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 w-full' } }),
elements: multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 w-full' } }),
effect: input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),
tags: multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 w-full' } }),
concentration: toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0' } }),
action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ])
};
return foldable([
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]),
], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-64' }), input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash', { noobserver: true }), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
}
const add = () => {
config.spells.push({
@@ -280,13 +343,19 @@ class SpellEditor extends BuilderTab
content = element;
};
const remove = (spell: SpellConfig) => {
config.spells = config.spells.filter(e => e !== spell);
confirm('Voulez vous vraiment supprimer ce sort ?').then(e => {
if(e)
{
config.spells = config.spells.filter(e => e !== spell);
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
}
});
}
const redraw = () => table(config.spells.map(render), { id: 'ID', name: 'Nom', rank: 'Rang', type: 'Type', cost: 'Coût', speed: 'Incantation', elements: 'Elements', effect: 'Effet', tags: 'Tag', concentration: 'Concentration', action: 'Actions' }, { class: { table: 'flex-1' } });
const redraw = () => div('flex flex-col divide-y', config.spells.map(render));
//, { class: { table: 'flex-1' } });
let content = redraw();
this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
}