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);
}
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;
}).finally(() => {
return f;
}).catch(() => feature).finally(() => {
setTimeout(popup.close, 150);
this._editor.container.setAttribute('data-state', 'inactive');
});
const popup = fullblocker([this._editor.container], {
priority: true, closeWhenOutside: false,
});
this._editor.container.setAttribute('data-state', 'active');
setTimeout(() => this._editor.container.setAttribute('data-state', 'active'), 1);
return promise;
}
private save()
@ -133,9 +134,14 @@ class TrainingEditor extends BuilderTab
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) => 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]!);
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ])))
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 } }));
});
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]);
return element;
})),
])
}
@ -357,48 +363,50 @@ export class FeatureEditor
switch(effect.category)
{
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':
return flattenFeatureChoices.find(e => e.category === 'choice');
return flattenFeatureChoices.findLast(e => e.category === 'choice');
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 idx = this._feature!.effect.findIndex(e => e.id === buffer.id);
const idx = this._feature!.effect.findIndex(e => e.id === _buffer.id);
if(idx === -1)
this._feature!.effect.push(buffer);
this._feature!.effect.push(_buffer);
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 = () => {
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)
content.remove();
else
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>) => {
let top: NodeChildren = [], bottom: NodeChildren = [];
switch(buffer.category)
{
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));
let valueSelection = valueVariable();
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]' } }),
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); } }),
button(icon('radix-icons:update'), () => {
valueSelection,
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);
const element = redraw();
this._table.replaceChild(element, content);
content = element;
const newValueSelection = valueVariable();
valueSelection?.parentElement?.replaceChild(newValueSelection, valueSelection);
valueSelection = newValueSelection;
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]) ];
break;
@ -424,23 +432,23 @@ export class FeatureEditor
const add = () => {
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);
list.appendChild(render(option));
list.appendChild(render(option, true));
};
const remove = (option: FeatureEffect) => {
};
const render = (option: FeatureEffect): HTMLElement => {
const render = (option: FeatureEffect & { text: string }, state: boolean): HTMLElement => {
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) => {
option = { id: option.id, ...e } as FeatureEffect;
const element = render(option);
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 & { text: string };
const element = render(option, true);
_content?.parentElement?.replaceChild(element, _content);
_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;
}
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') ];
bottom = [ list ];
break;
@ -449,13 +457,13 @@ export class FeatureEditor
return { top, bottom };
}
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', [
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) => {
buffer = { id: buffer.id, ...e } as FeatureItem;
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;
const element = redraw();
this._table.replaceChild(element, content);
content?.parentElement?.replaceChild(element, content);
content = element;
} }),
...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 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 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: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, },
@ -623,7 +660,7 @@ function textFromEffect(effect: Partial<FeatureItem>): string
}
else if(effect.category === 'choice')
{
return `${effect.text} (${effect.options?.length ?? 0} options)`;
return `${effect.text} (${effect.options?.length ?? 0} options).`;
}
return `Inconnu`;