Nearly finished FeatureEditor for choices

This commit is contained in:
Clément Pons 2025-08-20 22:25:47 +02:00
parent 06276b3fbc
commit 658499749d
5 changed files with 76 additions and 5482 deletions

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -63,18 +63,19 @@ export class HomebrewBuilder
this._content?.replaceChildren(...this._tabsContent[tab]!.dom); this._content?.replaceChildren(...this._tabsContent[tab]!.dom);
} }
edit(feature: Feature) edit(feature: Feature): Promise<Feature>
{ {
const promise = this._editor.edit(feature).then(f => { const promise: Promise<Feature> = this._editor.edit(feature).then(f => {
this._config.features[feature.id] = f; this._config.features[feature.id] = f;
}).finally(() => { return f;
}).catch(() => feature).finally(() => {
setTimeout(popup.close, 150); setTimeout(popup.close, 150);
this._editor.container.setAttribute('data-state', 'inactive'); this._editor.container.setAttribute('data-state', 'inactive');
}); });
const popup = fullblocker([this._editor.container], { const popup = fullblocker([this._editor.container], {
priority: true, closeWhenOutside: false, priority: true, closeWhenOutside: false,
}); });
this._editor.container.setAttribute('data-state', 'active'); setTimeout(() => this._editor.container.setAttribute('data-state', 'active'), 1);
return promise; return promise;
} }
private save() private save()
@ -133,9 +134,14 @@ class TrainingEditor extends BuilderTab
const statRenderBlock = (stat: MainStat) => { const statRenderBlock = (stat: MainStat) => {
return Object.entries(config.training[stat]).map( 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]) ])]), (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) => 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 => { div("flex flex-row gap-4 justify-center", level[1].map((option, j) => {
this._builder.edit(config.features[option]!); 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 => {
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]))) this._builder.edit(config.features[option]!).then(e => {
element.replaceChildren(markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }));
});
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
})),
]) ])
} }
@ -357,48 +363,50 @@ export class FeatureEditor
switch(effect.category) switch(effect.category)
{ {
case 'value': case 'value':
return flattenFeatureChoices.find(e => e.category === 'value' && e.property === effect.property); return flattenFeatureChoices.findLast(e => e.category === 'value' && e.property === effect.property);
case 'choice': case 'choice':
return flattenFeatureChoices.find(e => e.category === 'choice'); return flattenFeatureChoices.findLast(e => e.category === 'choice');
case 'list': case 'list':
return flattenFeatureChoices.find(e => e.category === 'list' && e.list === effect.list); return flattenFeatureChoices.findLast(e => e.category === 'list' && e.list === effect.list);
} }
}; };
const approve = () => { const approve = () => {
const idx = this._feature!.effect.findIndex(e => e.id === buffer.id); const idx = this._feature!.effect.findIndex(e => e.id === _buffer.id);
if(idx === -1) if(idx === -1)
this._feature!.effect.push(buffer); this._feature!.effect.push(_buffer);
else else
this._feature!.effect[idx] = buffer; this._feature!.effect[idx] = _buffer;
this._table.replaceChild(this._renderEffect(buffer), content); this._table.replaceChild(this._renderEffect(_buffer), content);
}, reject = () => { }, reject = () => {
const idx = this._feature!.effect.findIndex(e => e.id === buffer.id); const idx = this._feature!.effect.findIndex(e => e.id === _buffer.id);
if(idx === -1) if(idx === -1)
content.remove(); content.remove();
else else
this._table.replaceChild(this._renderEffect(effect), content); this._table.replaceChild(this._renderEffect(effect), content);
} }
let buffer = JSON.parse(JSON.stringify(effect)) as FeatureItem; let _buffer = JSON.parse(JSON.stringify(effect)) as FeatureItem;
const drawByCategory = (buffer: Partial<FeatureItem>) => { const drawByCategory = (buffer: Partial<FeatureItem>) => {
let top: NodeChildren = [], bottom: NodeChildren = []; let top: NodeChildren = [], bottom: NodeChildren = [];
switch(buffer.category) switch(buffer.category)
{ {
case 'value': case 'value':
const valueVariable = () => typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } });
const summaryText = text(textFromEffect(buffer)); const summaryText = text(textFromEffect(buffer));
let valueSelection = valueVariable();
top = [ top = [
select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property ?? '') || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }), select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property ?? '') || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }),
typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } }), valueSelection,
button(icon('radix-icons:update'), () => { tooltip(button(icon('radix-icons:update'), () => {
(buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0); (buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
const element = redraw(); const newValueSelection = valueVariable();
this._table.replaceChild(element, content); valueSelection?.parentElement?.replaceChild(newValueSelection, valueSelection);
content = element; valueSelection = newValueSelection;
summaryText.textContent = textFromEffect(buffer); summaryText.textContent = textFromEffect(buffer);
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), }, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Changer d\'editeur', 'bottom'),
]; ];
bottom = [ div('px-2 py-1 flex items-center flex-1', [summaryText]) ]; bottom = [ div('px-2 py-1 flex items-center flex-1', [summaryText]) ];
break; break;
@ -424,23 +432,23 @@ export class FeatureEditor
const add = () => { const add = () => {
const option: Extract<FeatureItem, { category: 'choice' }>["options"][number] = { id: getID(ID_SIZE), category: 'value', text: '', operation: 'add', property: '', value: 0 }; const option: Extract<FeatureItem, { category: 'choice' }>["options"][number] = { id: getID(ID_SIZE), category: 'value', text: '', operation: 'add', property: '', value: 0 };
(buffer as Extract<FeatureItem, { category: 'choice' }>).options.push(option); (buffer as Extract<FeatureItem, { category: 'choice' }>).options.push(option);
list.appendChild(render(option)); list.appendChild(render(option, true));
}; };
const remove = (option: FeatureEffect) => { const render = (option: FeatureEffect & { text: string }, state: boolean): HTMLElement => {
};
const render = (option: FeatureEffect): HTMLElement => {
const { top: _top, bottom: _bottom } = drawByCategory(option); const { top: _top, bottom: _bottom } = drawByCategory(option);
const combo = combobox(featureChoices.filter(e => (e?.value as FeatureItem)?.category !== 'choice'), { defaultValue: match(option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => { const combo = combobox([...featureChoices].filter(e => (e?.value as FeatureItem)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureItem)?.category !== 'choice') : e.value; return e; }), { defaultValue: match(option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => {
option = { id: option.id, ...e } as FeatureEffect; option = { id: option.id, ...e } as FeatureEffect & { text: string };
const element = render(option); const element = render(option, true);
_content?.parentElement?.replaceChild(element, _content); _content?.parentElement?.replaceChild(element, _content);
_content = element; _content = element;
} }); } });
let _content: HTMLElement = foldable(_bottom, [ div('flex flex-1 justify-between', [ div('flex flex-row',[ combo, ..._top ]), tooltip(button(icon('radix-icons:trash'), () => remove(option), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' } }); let _content: HTMLElement = foldable(_bottom, [ div('flex flex-1 justify-between', [ div('flex flex-1 flex-row',[ combo, ..._top, input('text', { defaultValue: option.text, input: (value) => option.text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1', placeholder: 'Description' }) ]), tooltip(button(icon('radix-icons:trash'), () => {
_content.remove();
(buffer as Extract<FeatureItem, { category: 'choice' }>).options = (buffer as Extract<FeatureItem, { category: 'choice' }>).options.filter(e => e.id !== option.id);
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' }, open: state });
return _content; return _content;
} }
const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => render(e)) ?? []); const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => render(e, false)) ?? []);
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract<FeatureItem, { category: 'choice' }>).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => add(), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Ajouter une option', 'bottom') ]; top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract<FeatureItem, { category: 'choice' }>).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => add(), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Ajouter une option', 'bottom') ];
bottom = [ list ]; bottom = [ list ];
break; break;
@ -449,13 +457,13 @@ export class FeatureEditor
return { top, bottom }; return { top, bottom };
} }
const redraw = () => { const redraw = () => {
const { top, bottom } = drawByCategory(buffer); const { top, bottom } = drawByCategory(_buffer);
return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [ return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [
div('flex flex-row flex-1', [ div('flex flex-row flex-1', [
combobox(featureChoices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => { combobox(featureChoices, { defaultValue: match(_buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => {
buffer = { id: buffer.id, ...e } as FeatureItem; _buffer = { id: _buffer.id, ...e } as FeatureItem;
const element = redraw(); const element = redraw();
this._table.replaceChild(element, content); content?.parentElement?.replaceChild(element, content);
content = element; content = element;
} }), } }),
...top, ...top,
@ -511,7 +519,36 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Modifier d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 0 } }, { text: 'Modifier d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 0 } },
{ text: 'Modifier de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 0 } }, { text: 'Modifier de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 0 } },
{ text: 'Modifier de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 0 } }, { text: 'Modifier de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 0 } },
{ text: 'Modifier de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 0 } } { text: 'Modifier de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 0 } },
//@ts-ignore
{ text: 'Modifier au choix', value: { category: 'choice', text: '+1 au modifier de ', options: [
{ text: 'Modifier de force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 },
{ text: 'Modifier de dextérité', category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 },
{ text: 'Modifier de constitution', category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 },
{ text: 'Modifier d\'intelligence', category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 },
{ text: 'Modifier de curiosité', category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 },
{ text: 'Modifier de charisme', category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 },
{ text: 'Modifier de psyché', egory: 'value', property: 'modifier/psyche', operation: 'add', value: 1 }
]}}
] },
{ text: 'Jet de résistance', value: [
{ text: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 0 } },
{ text: 'Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 0 } },
{ text: 'Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 0 } },
{ text: 'Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 0 } },
{ text: 'Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 0 } },
{ text: 'Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 0 } },
{ text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 0 } },
//@ts-ignore
{ text: 'Résistance au choix', value: { category: 'choice', text: '+1 au jet de résistance de ', options: [
{ text: 'Force', category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 },
{ text: 'Dextérité', category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 },
{ text: 'Constitution', category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 },
{ text: 'Intelligence', category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 },
{ text: 'Curiosité', category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 },
{ text: 'Charisme', category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 },
{ text: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }
]}}
] }, ] },
{ text: 'Action', value: { category: 'list', list: 'action', action: 'add' }, }, { text: 'Action', value: { category: 'list', list: 'action', action: 'add' }, },
{ text: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, }, { text: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, },
@ -623,7 +660,7 @@ function textFromEffect(effect: Partial<FeatureItem>): string
} }
else if(effect.category === 'choice') else if(effect.category === 'choice')
{ {
return `${effect.text} (${effect.options?.length ?? 0} options)`; return `${effect.text} (${effect.options?.length ?? 0} options).`;
} }
return `Inconnu`; return `Inconnu`;