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 { MarkdownEditor } from "./editor.util";
import { button, combobox, fakeA, foldable, input, numberpicker, select, type Option } from "./proses";
import { fullblocker, tooltip } from "./floating.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 renderMarkdown from "./markdown.util";
import { Tree } from "./tree";
import markdownUtil from "./markdown.util";
const tabTexts: Record<string, string> = {
};
const config = characterConfig as CharacterConfig;
export class HomebrewBuilder
{
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('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
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);
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") ])
@ -398,7 +396,7 @@ export class FeatureEditor
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]' } }),
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,
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);
@ -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 ]) ];
}
}
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;
case 'choice':
const add = () => {
@ -482,63 +489,67 @@ export class FeatureEditor
}
const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 0 }, },
{ text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 0 }, },
{ text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 0 }, },
{ text: 'Nombre d\'œuvres maitrisés', value: { category: 'value', property: 'artslots', operation: 'add', value: 0 }, },
{ text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 0 }, },
{ text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 0 }, },
{ text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 0 }, },
{ text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 0 }, },
{ text: 'Points de compétence', value: { category: 'value', property: 'ability', 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: 1 }, },
{ 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: 1 }, },
{ text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 1 }, },
{ text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 1 }, },
{ text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 1 }, },
{ 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: 1 }, },
{ 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 max', value: { category: 'value', property: 'defense/hardcap', operation: 'add', value: 0 } },
{ text: 'Défense fixe', value: { category: 'value', property: 'defense/static', operation: 'add', value: 0 } },
{ text: 'Parade active', value: { category: 'value', property: 'defense/activeparry', operation: 'add', value: 0 } },
{ text: 'Parade passive', value: { category: 'value', property: 'defense/passiveparry', operation: 'add', value: 0 } },
{ text: 'Esquive active', value: { category: 'value', property: 'defense/activedodge', operation: 'add', value: 0 } },
{ text: 'Esquive passive', value: { category: 'value', property: 'defense/passivedodge', 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: 1 } },
{ 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: 1 } },
{ 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: 1 } }
] },
{ text: 'Maitrise', value: [
{ text: 'Maitrise des armes (for.)', value: { category: 'value', property: 'mastery/strength', operation: 'add', value: 0 } },
{ text: 'Maitrise des armes (dex.)', value: { category: 'value', property: 'mastery/dexterity', operation: 'add', value: 0 } },
{ text: 'Maitrise des boucliers', value: { category: 'value', property: 'mastery/shield', operation: 'add', value: 0 } },
{ text: 'Maitrise des armure', value: { category: 'value', property: 'mastery/armor', operation: 'add', value: 0 } },
{ text: 'Attaque multiple', value: { category: 'value', property: 'mastery/multiattack', operation: 'add', value: 0 } },
{ text: 'Arbre de magie (Puissance)', value: { category: 'value', property: 'mastery/magicpower', operation: 'add', value: 0 } },
{ text: 'Arbre de magie (Rapidité)', value: { category: 'value', property: 'mastery/magicspeed', operation: 'add', value: 0 } },
{ text: 'Arbre de magie (Elements)', value: { category: 'value', property: 'mastery/magicelement', operation: 'add', value: 0 } },
{ text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', 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: 1 } },
{ 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: 1 } },
{ 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: 1 } },
{ 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: 1 } },
{ 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: 'Modifier', value: [
{ text: 'Modifier de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 0 } },
{ text: 'Modifier de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 0 } },
{ text: 'Modifier de constitution', value: { category: 'value', property: 'modifier/constitution', 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 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: '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: 'Modifieur', value: [
{ text: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } },
{ text: 'Modifieur de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } },
{ text: 'Modifieur de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } },
{ text: 'Modifieur d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } },
{ text: 'Modifieur de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } },
{ text: 'Modifieur de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } },
{ text: 'Modifieur de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } },
//@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: 'Modifieur au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [
{ text: 'Modifieur de force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 },
{ text: 'Modifieur de dextérité', category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 },
{ text: 'Modifieur de constitution', category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 },
{ text: 'Modifieur d\'intelligence', category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 },
{ text: 'Modifieur de curiosité', category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 },
{ text: 'Modifieur de charisme', category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 },
{ text: 'Modifieur 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 } },
{ 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: 1 } },
{ 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: 1 } },
{ 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: 1 } },
{ text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } },
//@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 },
@ -550,6 +561,14 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ 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: 'Réaction', value: { category: 'list', list: 'reaction', 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).`;
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).`;
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;
}
@ -591,7 +620,18 @@ function textFromEffect(effect: Partial<FeatureItem>): string
switch(splited[0])
{
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':
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).' });
default: return 'Maitrise inconnue.';
}
/* case 'resistance':
return splited[1] ? config.resistances[splited[1] as string].name : 'résistance inconnue'; */
case 'resistance':
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':
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':
@ -655,7 +695,7 @@ function textFromEffect(effect: Partial<FeatureItem>): string
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'}".`;
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')

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 ]);
}
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
{
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;
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)
return;
@ -250,14 +259,40 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
textValue.textContent = e.text;
settings?.change && settings?.change(e.value);
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: () => {
if(disabled)
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();
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') ]);
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 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 = () => {
if(disabled || (context && context.container.parentElement))
return;
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 });
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)
return;
const redrawn = render(option.item)?.container;
if(redrawn)
{
context.container.replaceChildren(redrawn);
context.container.replaceChildren(option.container);
tree.push(option);
}
focus();
};
const back = () => {
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));
};
const render = (option: Option<T>): StoredOption<T> | undefined => {
const render = (option: Option<T>, i: number): StoredOption<T> | undefined => {
if(option === undefined)
return;
@ -313,17 +352,17 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
{
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;
}
else
{
return { item: option, dom: dom('div', { listeners: { click: () => {
return { index: i, item: option, dom: dom('div', { listeners: { click: () => {
select.value = option.text;
settings?.change && settings?.change(option.value as T);
selected = true;
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[] => {
@ -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));
selected = false;
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' });
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 FeatureID = string;
export type Resistance = string;
export type Alignment = { loyalty: 'loyal' | 'neutral' | 'chaotic', kindness: 'good' | 'neutral' | 'evil' };
export type Character = {
@ -37,12 +38,14 @@ export type Character = {
export type CharacterVariables = {
health: number;
mana: number;
exhaustion: number;
sickness: Array<{ id: string, progress: number | true }>;
equipment: Array<string>;
};
export type CharacterConfig = {
peoples: RaceConfig[],
peoples: RaceConfig[];
resistances: Record<Resistance, { name: string, statistic: MainStat }>;
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
abilities: Record<Ability, AbilityConfig>;
spells: SpellConfig[];
@ -129,6 +132,11 @@ export type CompiledCharacter = {
speed: number | false;
capacity: number | false;
initiative: number;
exhaust: number;
itempower: number;
action: number;
reaction: number;
variables: CharacterVariables,