Various fixes to select, combobox and feature editor.

This commit is contained in:
Peaceultime 2025-08-24 23:35:57 +02:00
parent 658499749d
commit 247b14b2c8
7 changed files with 7067 additions and 78 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

@ -1,18 +1,16 @@
import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat } from "~/types/character"; import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance } from "~/types/character";
import { div, dom, icon, text, type NodeChildren } from "./dom.util"; import { div, dom, icon, text, type NodeChildren } from "./dom.util";
import { MarkdownEditor } from "./editor.util"; import { MarkdownEditor } from "./editor.util";
import { button, combobox, fakeA, foldable, input, numberpicker, select, type Option } from "./proses"; import { button, combobox, fakeA, foldable, input, numberpicker, select, type Option } from "./proses";
import { fullblocker, tooltip } from "./floating.util"; import { fullblocker, tooltip } from "./floating.util";
import { MAIN_STATS, mainStatShortTexts, mainStatTexts } from "./character.util"; import { MAIN_STATS, mainStatShortTexts, mainStatTexts } from "./character.util";
import config from "#shared/character-config.json"; import characterConfig from "#shared/character-config.json";
import { clamp, getID, ID_SIZE } from "./general.util"; import { clamp, getID, ID_SIZE } from "./general.util";
import renderMarkdown from "./markdown.util"; import renderMarkdown from "./markdown.util";
import { Tree } from "./tree"; import { Tree } from "./tree";
import markdownUtil from "./markdown.util"; import markdownUtil from "./markdown.util";
const tabTexts: Record<string, string> = { const config = characterConfig as CharacterConfig;
};
export class HomebrewBuilder export class HomebrewBuilder
{ {
private _container: HTMLDivElement; private _container: HTMLDivElement;
@ -350,7 +348,7 @@ export class FeatureEditor
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]), div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => { div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
this._table.replaceChild(this._edit(effect), content); this._table.replaceChild(this._edit(effect), content);
}, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifier", "bottom"), tooltip(button(icon('radix-icons:trash'), () => { }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifieur", "bottom"), tooltip(button(icon('radix-icons:trash'), () => {
this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id); this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id);
content.remove(); content.remove();
}, 'p-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") ]) }, 'p-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") ])
@ -398,7 +396,7 @@ export class FeatureEditor
const summaryText = text(textFromEffect(buffer)); const summaryText = text(textFromEffect(buffer));
let valueSelection = valueVariable(); 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([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].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]' } }),
valueSelection, valueSelection,
tooltip(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);
@ -426,7 +424,16 @@ export class FeatureEditor
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1 flex items-center', [ editor.dom ]) ]; bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1 flex items-center', [ editor.dom ]) ];
} }
} }
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove', class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ]; else
{
bottom = [ select(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map(e => ({ text: buffer.list !== 'spells' ? (e as Extract<FeatureItem, { category: 'list' }>).item : config.spells.find(f => f.id === (e as Extract<FeatureItem, { category: 'list' }>).item)?.name ?? '', value: e.id })), { defaultValue: buffer.item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' } }) ];
}
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => {
(buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove';
const element = redraw();
content?.parentElement?.replaceChild(element, content);
content = element;
}, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ];
break; break;
case 'choice': case 'choice':
const add = () => { const add = () => {
@ -482,63 +489,67 @@ export class FeatureEditor
} }
const featureChoices: Option<Partial<FeatureItem>>[] = [ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 0 }, }, { text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 1 }, },
{ text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 0 }, }, { text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 1 }, },
{ text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 0 }, }, { text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 1 }, },
{ text: 'Nombre d\'œuvres maitrisés', value: { category: 'value', property: 'artslots', operation: 'add', value: 0 }, }, { text: 'Nombre d\'œuvres maitrisés', value: { category: 'value', property: 'artslots', operation: 'add', value: 1 }, },
{ text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 0 }, }, { text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 1 }, },
{ text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 0 }, }, { text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 1 }, },
{ text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 0 }, }, { text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 1 }, },
{ text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 0 }, }, { text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 1 }, },
{ text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 0 }, }, { text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 1 }, },
{ text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, }, { text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, },
{ text: 'Point d\'action', value: { category: 'value', property: 'action', operation: 'set', value: 1 }, },
{ text: 'Point de réaction', value: { category: 'value', property: 'reaction', operation: 'set', value: 1 }, },
{ text: 'Puissance magique', value: { category: 'value', property: 'itempower', operation: 'add', value: 1 }, },
{ text: 'Spécialisation', value: { category: 'value', property: 'spec', operation: 'add', value: 1 }, },
{ text: 'Défense', value: [ { text: 'Défense', value: [
{ text: 'Défense max', value: { category: 'value', property: 'defense/hardcap', operation: 'add', value: 0 } }, { text: 'Défense max', value: { category: 'value', property: 'defense/hardcap', operation: 'add', value: 1 } },
{ text: 'Défense fixe', value: { category: 'value', property: 'defense/static', operation: 'add', value: 0 } }, { text: 'Défense fixe', value: { category: 'value', property: 'defense/static', operation: 'add', value: 1 } },
{ text: 'Parade active', value: { category: 'value', property: 'defense/activeparry', operation: 'add', value: 0 } }, { text: 'Parade active', value: { category: 'value', property: 'defense/activeparry', operation: 'add', value: 1 } },
{ text: 'Parade passive', value: { category: 'value', property: 'defense/passiveparry', operation: 'add', value: 0 } }, { text: 'Parade passive', value: { category: 'value', property: 'defense/passiveparry', operation: 'add', value: 1 } },
{ text: 'Esquive active', value: { category: 'value', property: 'defense/activedodge', operation: 'add', value: 0 } }, { text: 'Esquive active', value: { category: 'value', property: 'defense/activedodge', operation: 'add', value: 1 } },
{ text: 'Esquive passive', value: { category: 'value', property: 'defense/passivedodge', operation: 'add', value: 0 } } { text: 'Esquive passive', value: { category: 'value', property: 'defense/passivedodge', operation: 'add', value: 1 } }
] }, ] },
{ text: 'Maitrise', value: [ { text: 'Maitrise', value: [
{ text: 'Maitrise des armes (for.)', value: { category: 'value', property: 'mastery/strength', operation: 'add', value: 0 } }, { text: 'Maitrise des armes (for.)', value: { category: 'value', property: 'mastery/strength', operation: 'add', value: 1 } },
{ text: 'Maitrise des armes (dex.)', value: { category: 'value', property: 'mastery/dexterity', operation: 'add', value: 0 } }, { text: 'Maitrise des armes (dex.)', value: { category: 'value', property: 'mastery/dexterity', operation: 'add', value: 1 } },
{ text: 'Maitrise des boucliers', value: { category: 'value', property: 'mastery/shield', operation: 'add', value: 0 } }, { text: 'Maitrise des boucliers', value: { category: 'value', property: 'mastery/shield', operation: 'add', value: 1 } },
{ text: 'Maitrise des armure', value: { category: 'value', property: 'mastery/armor', operation: 'add', value: 0 } }, { text: 'Maitrise des armure', value: { category: 'value', property: 'mastery/armor', operation: 'add', value: 1 } },
{ text: 'Attaque multiple', value: { category: 'value', property: 'mastery/multiattack', operation: 'add', value: 0 } }, { text: 'Attaque multiple', value: { category: 'value', property: 'mastery/multiattack', operation: 'add', value: 1 } },
{ text: 'Arbre de magie (Puissance)', value: { category: 'value', property: 'mastery/magicpower', operation: 'add', value: 0 } }, { text: 'Arbre de magie (Puissance)', value: { category: 'value', property: 'mastery/magicpower', operation: 'add', value: 1 } },
{ text: 'Arbre de magie (Rapidité)', value: { category: 'value', property: 'mastery/magicspeed', operation: 'add', value: 0 } }, { text: 'Arbre de magie (Rapidité)', value: { category: 'value', property: 'mastery/magicspeed', operation: 'add', value: 1 } },
{ text: 'Arbre de magie (Elements)', value: { category: 'value', property: 'mastery/magicelement', operation: 'add', value: 0 } }, { text: 'Arbre de magie (Elements)', value: { category: 'value', property: 'mastery/magicelement', operation: 'add', value: 1 } },
{ text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', operation: 'add', value: 0 } } { text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', operation: 'add', value: 1 } }
] }, ] },
{ text: 'Compétence', value: Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 0 } })) }, { text: 'Compétence', value: Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) },
{ text: 'Modifier', value: [ { text: 'Modifieur', value: [
{ text: 'Modifier de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 0 } }, { text: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } },
{ text: 'Modifier de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 0 } }, { text: 'Modifieur de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } },
{ text: 'Modifier de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 0 } }, { text: 'Modifieur de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } },
{ text: 'Modifier d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 0 } }, { text: 'Modifieur d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } },
{ text: 'Modifier de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 0 } }, { text: 'Modifieur de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } },
{ text: 'Modifier de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 0 } }, { text: 'Modifieur de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } },
{ text: 'Modifier de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 0 } }, { text: 'Modifieur de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } },
//@ts-ignore //@ts-ignore
{ text: 'Modifier au choix', value: { category: 'choice', text: '+1 au modifier de ', options: [ { text: 'Modifieur au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [
{ text: 'Modifier de force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 }, { text: 'Modifieur 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: 'Modifieur 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: 'Modifieur 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: 'Modifieur 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: 'Modifieur 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: 'Modifieur 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: 'Modifieur de psyché', egory: 'value', property: 'modifier/psyche', operation: 'add', value: 1 }
]}} ]}}
] }, ] },
{ text: 'Jet de résistance', value: [ { text: 'Jet de résistance', value: [
{ text: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 0 } }, { text: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } },
{ text: 'Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 0 } }, { text: 'Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } },
{ text: 'Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 0 } }, { text: 'Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 } },
{ text: 'Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 0 } }, { text: 'Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 } },
{ text: 'Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 0 } }, { text: 'Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 } },
{ text: 'Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 0 } }, { text: 'Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } },
{ text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 0 } }, { text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } },
//@ts-ignore //@ts-ignore
{ text: 'Résistance au choix', value: { category: 'choice', text: '+1 au jet de résistance de ', options: [ { 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: 'Force', category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 },
@ -550,6 +561,14 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } { text: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }
]}} ]}}
] }, ] },
{ text: 'Bonus', value: Object.keys(config.resistances).map((e: Resistance) => ({ text: config.resistances[e]!.name, value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) },
{ text: 'Rang', value: [
{ text: 'Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } },
{ text: 'Sorts de savoir', value: { category: 'value', property: 'spellranks/knowledge', operation: 'add', value: 1 } },
{ text: 'Sorts d\'instinct', value: { category: 'value', property: 'spellranks/instinct', operation: 'add', value: 1 } },
{ text: 'Œuvres', value: { category: 'value', property: 'spellranks/arts', operation: 'add', value: 1 } },
] },
{ text: 'Fatigue supportable', value: { category: 'value', property: 'exhaust', 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' }, },
{ text: 'Action libre', value: { category: 'list', list: 'freeaction', action: 'add' }, }, { text: 'Action libre', value: { category: 'list', list: 'freeaction', action: 'add' }, },
@ -584,6 +603,16 @@ function textFromEffect(effect: Partial<FeatureItem>): string
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) d\'entrainement.' } }) : `Opération interdite (Entrainement fixe).`; return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) d\'entrainement.' } }) : `Opération interdite (Entrainement fixe).`;
case 'ability': case 'ability':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) de compétence.' } }) : `Opération interdite (Compétences fixe).`; return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) de compétence.' } }) : `Opération interdite (Compétences fixe).`;
case 'spec':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' spécialisation(s).' } }) : `Opération interdite (Spécialisation fixe).`;
case 'itempower':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' puissance magique supportable.' } }) : `Opération interdite (Puissance magique fixe).`;
case 'action':
return effect.operation === 'add' ? `Opération interdite (Point d'action bonus).` : textFromValue(effect.value, { suffix: { truely: ' point(s) d\'action par tour.' }, falsely: 'Opération interdite (Action = interdit).' });
case 'reaction':
return effect.operation === 'add' ? `Opération interdite (Point de réaction bonus).` : textFromValue(effect.value, { suffix: { truely: ' point(s) de réaction par tour.' }, falsely: 'Opération interdite (Réaction = interdit).' });
case 'exhaust':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Vous êtes capable de supporter ', positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) de fatigue avant de subir les effets de la fatigue.' } }) : `Opération interdite (Fatigue fixe).`;
default: break; default: break;
} }
@ -591,7 +620,18 @@ function textFromEffect(effect: Partial<FeatureItem>): string
switch(splited[0]) switch(splited[0])
{ {
case 'spellranks': case 'spellranks':
return ''; switch(splited[1])
{
case 'precision':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang de sort de précision.' } }) : `Opération interdite (Rang de sorts de précision fixe).`;
case 'knowledge':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang de sort de savoir.' } }) : `Opération interdite (Rang de sorts de savoir fixe).`;
case 'instinct':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang de sort d\'instinct.' } }) : `Opération interdite (Rang de sorts d\'instinct fixe).`;
case 'arts':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang d\'œuvres.' } }) : `Opération interdite (Rang d\'œuvres fixe).`;
default: return 'Type de sort inconnu.';
}
case 'defense': case 'defense':
switch(splited[1]) switch(splited[1])
{ {
@ -632,8 +672,8 @@ function textFromEffect(effect: Partial<FeatureItem>): string
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise instinct = interdit).' }); return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise instinct = interdit).' });
default: return 'Maitrise inconnue.'; default: return 'Maitrise inconnue.';
} }
/* case 'resistance': case 'resistance':
return splited[1] ? config.resistances[splited[1] as string].name : 'résistance inconnue'; */ return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${config.resistances[splited[1] as Resistance]!.name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${config.resistances[splited[1] as Resistance]!.name} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (${config.resistances[splited[1] as Resistance]!.name} = interdit).` });
case 'abilities': case 'abilities':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${`${config.abilities[splited[1] as Ability].name}.`}` }); return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${`${config.abilities[splited[1] as Ability].name}.`}` });
case 'modifier': case 'modifier':
@ -655,7 +695,7 @@ function textFromEffect(effect: Partial<FeatureItem>): string
case 'spells': case 'spells':
return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".`; return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".`;
case 'sickness': case 'sickness':
return effect.action === 'add' ? `Vous subisez en permanence la maladie "${config.lists.sickness.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".` : `Vous ne subisez plus la maladie "${config.lists.sickness.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".`; return effect.action === 'add' ? `Vous subisez en permanence la maladie "${config.lists.sickness?.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".` : `Vous ne subisez plus la maladie "${config.lists.sickness?.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".`;
} }
} }
else if(effect.category === 'choice') else if(effect.category === 'choice')

View File

@ -235,14 +235,23 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]); disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
} }
export type Option<T> = { text: string, value: T | Option<T>[] } | undefined; export type Option<T> = { text: string, value: T | Option<T>[] } | undefined;
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T> | undefined> }; type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T> | undefined>, index: number };
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
{ {
let close: Function | undefined; let context: { close: Function };
let focused: number | undefined;
options = options.filter(e => !!e);
const focus = (i?: number) => {
focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false);
i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
focused = i;
}
let disabled = settings?.disabled ?? false; let disabled = settings?.disabled ?? false;
const textValue = text(options.find(e => Array.isArray(e) ? false : e?.value === settings?.defaultValue)?.text ?? ''); const textValue = text(options.find(e => Array.isArray(e) ? false : e?.value === settings?.defaultValue)?.text ?? '');
const optionElements = options.map(e => { const optionElements = options.map((e, i) => {
if(e === undefined) if(e === undefined)
return; return;
@ -250,14 +259,40 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
textValue.textContent = e.text; textValue.textContent = e.text;
settings?.change && settings?.change(e.value); settings?.change && settings?.change(e.value);
close && close(); close && close();
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]); }, 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: () => { const select = dom('div', { listeners: { click: () => {
if(disabled) if(disabled)
return; return;
const handleKeys = (e: KeyboardEvent) => {
switch(e.key.toLocaleLowerCase())
{
case 'arrowdown':
focus(clamp((focused ?? -1) + 1, 0, options.length - 1));
return;
case 'arrowup':
focus(clamp((focused ?? 1) - 1, 0, options.length - 1));
return;
case 'pageup':
focus(0);
return;
case 'pagedown':
focus(optionElements.length - 1);
return;
case 'enter':
focused && optionElements[focused]?.click();
return;
case 'escape':
context?.close();
return;
default: return;
}
}
window.addEventListener('keydown', handleKeys);
const box = select.getBoundingClientRect(); const box = select.getBoundingClientRect();
close = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` } }).close; context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
} }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]); } }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);
Object.defineProperty(select, 'disabled', { Object.defineProperty(select, 'disabled', {
@ -273,12 +308,19 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
{ {
let context: { container: HTMLElement, content: NodeChildren, close: () => void }; let context: { container: HTMLElement, content: NodeChildren, close: () => void };
let selected = true, tree: StoredOption<T>[] = []; let selected = true, tree: StoredOption<T>[] = [];
let focused: number | undefined;
const focus = (i?: number) => {
focused !== undefined && (tree.slice(-1)[0]?.children ?? optionElements)[focused]?.dom.toggleAttribute('data-focused', false);
i !== undefined && (tree.slice(-1)[0]?.children ?? optionElements)[i]?.dom.toggleAttribute('data-focused', true) && (tree.slice(-1)[0]?.children ?? optionElements)[i]?.dom.scrollIntoView({ behavior: 'instant', block: 'nearest' });
focused = i;
}
const show = () => { const show = () => {
if(disabled || (context && context.container.parentElement)) if(disabled || (context && context.container.parentElement))
return; return;
const box = container.getBoundingClientRect(); const box = container.getBoundingClientRect();
focus();
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements.map(e => e?.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: hide }); context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements.map(e => e?.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-y-auto overflow-x-hidden', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: hide });
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red'); if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
}; };
@ -292,12 +334,9 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
if(!context || !context.container.parentElement || option.container === undefined) if(!context || !context.container.parentElement || option.container === undefined)
return; return;
const redrawn = render(option.item)?.container; context.container.replaceChildren(option.container);
if(redrawn)
{
context.container.replaceChildren(redrawn);
tree.push(option); tree.push(option);
} focus();
}; };
const back = () => { const back = () => {
tree.pop(); tree.pop();
@ -305,7 +344,7 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
last ? context.container.replaceChildren(last.container ?? last.dom) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom)); last ? context.container.replaceChildren(last.container ?? last.dom) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
}; };
const render = (option: Option<T>): StoredOption<T> | undefined => { const render = (option: Option<T>, i: number): StoredOption<T> | undefined => {
if(option === undefined) if(option === undefined)
return; return;
@ -313,17 +352,17 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
{ {
const children = option.value.map(render); const children = option.value.map(render);
const stored = { item: option, dom: dom('div', { listeners: { click: () => progress(stored) }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer flex justify-between items-center', settings?.class?.option] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children }; const stored = { index: i, item: option, dom: dom('div', { listeners: { click: () => progress(stored), mouseenter: () => 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 flex justify-between items-center', settings?.class?.option] }, [ text(option.text), icon('radix-icons:caret-right', { width: 20, height: 20 }) ]), container: div('flex flex-1 flex-col', [div('flex flex-row justify-between items-center text-light-100 dark:text-dark-100 py-1 px-2 text-sm select-none sticky top-0 bg-light-20 dark:bg-dark-20 font-semibold', [button(icon('radix-icons:caret-left', { width: 16, height: 16 }), back, 'p-px'), text(option.text), div()]), div('flex flex-col flex-1', children.map(e => e?.dom))]), children };
return stored; return stored;
} }
else else
{ {
return { item: option, dom: dom('div', { listeners: { click: () => { return { index: i, item: option, dom: dom('div', { listeners: { click: () => {
select.value = option.text; select.value = option.text;
settings?.change && settings?.change(option.value as T); settings?.change && settings?.change(option.value as T);
selected = true; selected = true;
hide(); hide();
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(option.text) ]) }; }, mouseenter: () => 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(option.text) ]) };
} }
} }
const filter = (value: string, option?: StoredOption<T>): HTMLElement[] => { const filter = (value: string, option?: StoredOption<T>): HTMLElement[] => {
@ -347,6 +386,31 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
context && select.value ? context.container.replaceChildren(...optionElements.flatMap(e => filter(select.value.toLowerCase().trim().normalize(), e))) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom)); context && select.value ? context.container.replaceChildren(...optionElements.flatMap(e => filter(select.value.toLowerCase().trim().normalize(), e))) : context.container.replaceChildren(...optionElements.filter(e => !!e).map(e => e.dom));
selected = false; selected = false;
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red') if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
}, keydown: (e) => {
const opt = (tree.slice(-1)[0]?.item?.value as Option<T>[] ?? options.filter(e => !!e)).filter(e => !!e), elements = (tree.slice(-1)[0]?.children ?? optionElements);
switch(e.key.toLocaleLowerCase())
{
case 'arrowdown':
focus(clamp((focused ?? -1) + 1, 0, opt.length - 1));
return;
case 'arrowup':
focus(clamp((focused ?? 1) - 1, 0, opt.length - 1));
return;
case 'pageup':
focus(0);
return;
case 'pagedown':
focus(opt.length - 1);
return;
case 'enter':
focused && elements[focused]?.dom.click();
return;
case 'escape':
context?.close();
return;
default: return;
}
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' }); } }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
settings?.defaultValue && Tree.each(options, 'value', (item) => { if(item.value === settings?.defaultValue) select.value = item.text }); settings?.defaultValue && Tree.each(options, 'value', (item) => { if(item.value === settings?.defaultValue) select.value = item.text });

10
types/character.d.ts vendored
View File

@ -9,6 +9,7 @@ export type Category = typeof CATEGORIES[number];
export type SpellElement = typeof SPELL_ELEMENTS[number]; export type SpellElement = typeof SPELL_ELEMENTS[number];
export type FeatureID = string; export type FeatureID = string;
export type Resistance = string;
export type Alignment = { loyalty: 'loyal' | 'neutral' | 'chaotic', kindness: 'good' | 'neutral' | 'evil' }; export type Alignment = { loyalty: 'loyal' | 'neutral' | 'chaotic', kindness: 'good' | 'neutral' | 'evil' };
export type Character = { export type Character = {
@ -37,12 +38,14 @@ export type Character = {
export type CharacterVariables = { export type CharacterVariables = {
health: number; health: number;
mana: number; mana: number;
exhaustion: number;
sickness: Array<{ id: string, progress: number | true }>; sickness: Array<{ id: string, progress: number | true }>;
equipment: Array<string>; equipment: Array<string>;
}; };
export type CharacterConfig = { export type CharacterConfig = {
peoples: RaceConfig[], peoples: RaceConfig[];
resistances: Record<Resistance, { name: string, statistic: MainStat }>;
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>; training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
abilities: Record<Ability, AbilityConfig>; abilities: Record<Ability, AbilityConfig>;
spells: SpellConfig[]; spells: SpellConfig[];
@ -129,6 +132,11 @@ export type CompiledCharacter = {
speed: number | false; speed: number | false;
capacity: number | false; capacity: number | false;
initiative: number; initiative: number;
exhaust: number;
itempower: number;
action: number;
reaction: number;
variables: CharacterVariables, variables: CharacterVariables,