Rollback CharacterEditor to the previous version

This commit is contained in:
2026-06-10 13:29:33 +02:00
parent f9e0473b2a
commit bc1839c5e3
14 changed files with 1864 additions and 959 deletions

View File

@@ -2,7 +2,7 @@ import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemCon
import { div, dom, icon, span, text, type NodeChildren } from "#shared/dom";
import { MarkdownEditor } from "#shared/editor";
import { preview } from "#shared/proses";
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, tagpicker, toggle, type Option } from "#shared/components";
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, tagpicker, toggle, type Option, type RecurrentOption } from "#shared/components";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, colorByRarity, craftingText, DAMAGE_TYPES, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, masteryTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, subnameFactory, weaponTypeTexts } from "#shared/character";
import characterConfig from "#shared/character-config.json";
@@ -246,7 +246,7 @@ export class HomebrewBuilder
span('flex-1', `Rang ${spell.rank}`),
span('flex-1', spellTypeTexts[spell.type]),
span('flex-1', `${spell.cost} mana`),
span('flex-1', spell.speed === 'action' ? 'Action' : spell.speed === 'reaction' ? 'Réaction' : `${spell.speed} minutes`),
span('flex-1', spell.speed === 'action' ? 'Action' : spell.speed === 'reaction' ? 'Réaction' : spell.speed === 'channeling' ? 'Canalisation' : `${spell.speed} minutes`),
span('flex-1', spell.elements.length === 0 ? '' : `${elementTexts[spell.elements[0]!].text}${spell.elements.length > 1 ? ` (+${spell.elements.length - 1})` : ''}`),
span('flex-1', spell.range === 'personnal' ? 'Personnel' : spell.range === 0 ? 'Toucher' : `${spell.range} cases`),
span('flex-1', `${spell.tags && spell.tags.length > 0 ? spellTagTexts[spell.tags[0]!] : ''}${spell.tags && spell.tags.length > 1 ? ` (+${spell.tags.length - 1})` : ''}`),
@@ -271,7 +271,7 @@ export class HomebrewBuilder
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Type'), select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]),
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Incantation'), select<'action' | 'reaction' | number>(() => [{ text: 'Action', value: 'action' }, spell.type === 'instinct' ? { text: 'Reaction', value: 'reaction' } : undefined, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Incantation'), select<'action' | 'reaction' | 'channeling' | number>(() => [{ text: 'Action', value: 'action' }, spell.type === 'instinct' ? { text: 'Reaction', value: 'reaction' } : undefined, { text: 'Canalisation', value: 'channeling' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Portée'), select<'personnal' | number>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { change: (value) => spell.range = value, defaultValue: spell.range, class: { container: '!m-0 !h-9 w-full' } }), ]),
div('flex flex-col items-center justify-between gap-2 flex-1 *:text-center', [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]),
@@ -800,7 +800,7 @@ class FeatureEditor
}
private editList(buffer: Partial<FeatureList>)
{
let list: Option<string>[] = [];
let list: RecurrentOption<string>[] = [];
switch(buffer.list)
{
case undefined:
@@ -868,7 +868,7 @@ class FeatureEditor
}
private editChoice(buffer: Partial<FeatureChoice>)
{
const availableChoices: Option<Partial<FeatureValue | FeatureList | FeatureTree>>[] = featureChoices.filter(e => (e?.value as FeatureOption)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureOption)?.category !== 'choice') : e.value; return e; }) as Option<Partial<FeatureValue | FeatureList | FeatureTree>>[];
const availableChoices: RecurrentOption<Partial<FeatureValue | FeatureList | FeatureTree>>[] = featureChoices.filter(e => (e?.value as FeatureOption)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureOption)?.category !== 'choice') : e.value; return e; }) as RecurrentOption<Partial<FeatureValue | FeatureList | FeatureTree>>[];
const addChoice = () => {
const choice: { text: string; effects: (Partial<FeatureValue | FeatureList | FeatureTree>)[]; } = { effects: [{ id: getID() }], text: '' };
buffer.options ??= [];
@@ -1109,7 +1109,7 @@ export class ImprovementPanel
div('flex flex-row gap-2 items-center justify-between', [ span('flex flex-row gap-2 items-center', 'Fabrication'), div('flex flex-row items-center gap-2 !w-2/3', [ select<CraftingType>(Object.entries(craftingText).map(e => ({ text: e[1], value: e[0] as CraftingType })), { defaultValue: _improvement.craft.ability ?? 'crafter', change: (v) => _improvement.craft.ability = v, class: { container: '!w-1/2' } }), numberpicker({ defaultValue: _improvement.craft.difficulty ?? 0, input: (v) => _improvement.craft.difficulty = v, class: 'w-12' }) ]) ]),
], [ span('text-lg font-bold', "Propriétés"), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Rareté'), ]), select(Object.keys(rarityText).map(e => ({ text: rarityText[e as Rarity], value: e as Rarity })), { defaultValue: _improvement.rarity, change: (v) => _improvement.rarity = v, class: { container: '!w-1/2' } }), ]) ], { class: { content: 'group-data-[active]:grid grid-cols-2 my-2 gap-4', title: 'grid grid-cols-2 gap-4 mx-2', container: 'pb-2 border-b border-light-35 dark:border-dark-35' }, open: true } ),
foldable([ div('p-1 border border-light-40 dark:border-dark-40 w-full bg-light-25 dark:bg-dark-25 min-h-48 max-h-[32rem]', [ ImprovementPanel.descriptionEditor.dom ])], [ span('text-lg font-bold px-2', "Description des effets") ], { class: { container: 'gap-4 pb-2 border-b border-light-35 dark:border-dark-35' }, open: true, }),
div('flex flex-row gap-4 items-center pb-2 border-b border-light-35 dark:border-dark-35', [ dom('h3', { class: 'text-lg font-bold', text: 'Restrictions' }), tagpicker([], { defaultValue: Object.keys(improvement.restrictions ?? {}), class: { container: 'data-[focused]:shadow-raw transition-[box-shadow] data-[focused]:shadow-light-40 dark:data-[focused]:shadow-dark-40' } }) ]),
div('flex flex-row gap-4 items-center pb-2 border-b border-light-35 dark:border-dark-35', [ dom('h3', { class: 'text-lg font-bold', text: 'Restrictions' }), tagpicker(() => [...fixedItemTags, ...Object.values(config.items).filter(e => e.variants === undefined).map(e => ({ text: e.name, value: e.id }))], { defaultValue: Object.keys(_improvement.restrictions ?? {}), change: (v) => _improvement.restrictions = v.length === 0 ? undefined : Object.fromEntries(v.map(e => [e, true])), fill: 'cover' }) ]),
foldable([ effectContainer ], [ dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => {
const f = { id: getID(), };
@@ -1136,7 +1136,27 @@ export class ImprovementPanel
}
}
const featureChoices: Option<Partial<FeatureOption>>[] = [
export const fixedItemTags: Option<string>[] = [
{ text: 'Objet inerte', value: 'mundane' },
{ text: 'Objet magique', value: 'wondrous' },
{ text: 'Armure', value: 'armor' },
{ text: 'Armure légère', value: 'armor/light' },
{ text: 'Armure standard', value: 'armor/medium' },
{ text: 'Armure lourde', value: 'armor/heavy' },
{ text: 'Arme', value: 'weapon' },
{ text: 'Arme légère', value: 'weapon/light' },
{ text: 'Bouclier', value: 'weapon/shield' },
{ text: 'Arme lourde', value: 'weapon/heavy' },
{ text: 'Arme arme', value: 'weapon/classic' },
{ text: 'Arme de jet', value: 'weapon/throw' },
{ text: 'Arme naturelle', value: 'weapon/natural' },
{ text: 'Arme à deux mains', value: 'weapon/twohanded' },
{ text: 'Arme maniable', value: 'weapon/finesse' },
{ text: 'Arme longue', value: 'weapon/reach' },
{ text: 'Arme à projectile', value: 'weapon/projectile' },
{ text: 'Arme improvisée', value: 'weapon/improvised' }
]
const featureChoices: RecurrentOption<Partial<FeatureOption>>[] = [
{ 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 }, },
@@ -1164,7 +1184,7 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
{ text: 'Arbre', value: { category: 'tree' } },
{ text: 'Maitrise', value: Object.keys(masteryTexts).map(e => ({ text: `Maitrise > ${masteryTexts[e as keyof typeof masteryTexts].text}`, value: { category: 'list', action: 'add', list: 'mastery', item: e } })) },
{ text: 'Compétences', value: [
...ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
...ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as RecurrentOption<Partial<FeatureItem>>[],
{ text: 'Max de compétence', value: ABILITIES.map((e) => ({ text: `Max > ${abilityTexts[e as Ability]}`, value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
] },
{ text: 'Modifieur', value: [
@@ -1201,13 +1221,13 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
{ text: 'Résistance > Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } },
{ text: 'Résistance > Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } },
{ text: 'Résistance au choix', value: { category: 'choice', text: '+1 au jet de résistance de ', options: [
{ text: 'Résistance > Force', effects: [{ category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 }] },
{ text: 'Résistance > Dextérité', effects: [{ category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 }] },
{ text: 'Résistance > Constitution', effects: [{ category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 }] },
{ text: 'Résistance > Intelligence', effects: [{ category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 }] },
{ text: 'Résistance > Curiosité', effects: [{ category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 }] },
{ text: 'Résistance > Charisme', effects: [{ category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 }] },
{ text: 'Résistance > Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }] }
{ text: 'Résistance > Force', effects: [{ category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Résistance > Dextérité', effects: [{ category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Résistance > Constitution', effects: [{ category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Résistance > Intelligence', effects: [{ category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Résistance > Curiosité', effects: [{ category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Résistance > Charisme', effects: [{ category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Résistance > Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } as Partial<FeatureValue>] }
]} as Partial<FeatureChoice>}
] },
{ text: 'Difficulté des chocs', value: RESISTANCES.map(e => ({ text: `Difficulté > ${resistanceTexts[e]}`, value: { category: 'value', property: `bonus/resistance/${e}`, operation: 'add', value: 1 } })) },
@@ -1222,9 +1242,9 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
{ text: 'Bonus > Savoir', value: { category: 'value', property: 'bonus/spells/type/knowledge', operation: 'add', value: 1 } },
{ text: 'Bonus > Instinct', value: { category: 'value', property: 'bonus/spells/type/instinct', operation: 'add', value: 1 } },
{ text: 'Bonus > Choix par type', value: { category: 'choice', options: [
{ text: 'Précision', effects: [{ category: 'value', property: 'bonus/spells/type/precision', operation: 'add', value: 1 }] },
{ text: 'Savoir', effects: [{ category: 'value', property: 'bonus/spells/type/knowledge', operation: 'add', value: 1 }] },
{ text: 'Instinct', effects: [{ category: 'value', property: 'bonus/spells/type/instinct', operation: 'add', value: 1 }] },
{ text: 'Précision', effects: [{ category: 'value', property: 'bonus/spells/type/precision', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Savoir', effects: [{ category: 'value', property: 'bonus/spells/type/knowledge', operation: 'add', value: 1 } as Partial<FeatureValue>] },
{ text: 'Instinct', effects: [{ category: 'value', property: 'bonus/spells/type/instinct', operation: 'add', value: 1 } as Partial<FeatureValue>] },
] } as Partial<FeatureChoice> }
]},
{ text: 'Bonus par rang', value: [
@@ -1239,8 +1259,8 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
] } }
] as Partial<FeatureChoice> },
{ text: 'Bonus par élément', value: [
...SPELL_ELEMENTS.map(e => ({ text: `Bonus > ${elementTexts[e].text}`, value: { category: 'value', property: `bonus/spells/elements/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
{ text: 'Bonus > Choix par élément', value: { category: 'choice', options: SPELL_ELEMENTS.map(e => ({ text: elementTexts[e].text, effects: [{ category: 'value', property: `bonus/spells/elements/${e}`, operation: 'add', value: 1 }] })) } as Partial<FeatureChoice> }
...SPELL_ELEMENTS.map(e => ({ text: `Bonus > ${elementTexts[e].text}`, value: { category: 'value', property: `bonus/spells/elements/${e}`, operation: 'add', value: 1 } })) as RecurrentOption<Partial<FeatureItem>>[],
{ text: 'Bonus > Choix par élément', value: { category: 'choice', options: SPELL_ELEMENTS.map(e => ({ text: elementTexts[e].text, effects: [{ category: 'value', property: `bonus/spells/elements/${e}`, operation: 'add', value: 1 } as Partial<FeatureValue>] })) } as Partial<FeatureChoice> }
] },
] },
{ text: 'Aspect', value: [