import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character"; 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, toggle, type Option } from "#shared/components"; import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating"; import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, categoryText, colorByRarity, 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"; import { getID } from "#shared/general"; import markdown, { markdownReference, renderMDAsText } from "#shared/markdown"; import { Tree } from "#shared/tree"; import { getText, setText } from "#shared/i18n"; import { reactive } from "./reactive"; type Category = ItemConfig['category']; type Rarity = ItemConfig['rarity']; const config = reactive(characterConfig as CharacterConfig); export class HomebrewBuilder { private _container: HTMLElement; private _tabs: HTMLElement; constructor(container: HTMLElement) { this._container = container; this._tabs = tabgroup([ { id: 'peoples', title: [ text("Peuples") ], content: () => this.peoples() }, { id: 'training', title: [ text("Entrainement") ], content: () => this.training() }, { id: 'spells', title: [ text("Sorts") ], content: () => this.spells() }, { id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() }, { id: 'actions', title: [ text("Actions") ], content: () => this.actions() }, { id: 'items', title: [ text("Objets") ], content: () => this.items() }, { id: 'trees', title: [ text("Arbres") ], content: () => this.trees() }, ], { focused: 'training', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } }); this._tabs.children[0]?.appendChild(tooltip(button(icon('radix-icons:clipboard'), () => this.save(), 'p-1'), 'Copier', 'bottom')); this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [ this._tabs ])); } peoples() { const add = () => { const people: RaceConfig = { id: getID(), name: '', description: '', options: LEVELS.map(e => { const feature: Feature = { id: getID(), description: getID(), effect: [], } config.features[feature.id] = feature; return [e, [feature.id]] as [Level, string[]]; }).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record) }; config.peoples[people.id] = people; (content[0] as HTMLElement).appendChild(peopleRender(people)); } const render = (people: string, level: Level, feature: string) => { let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => { FeaturePanel.edit(config.features[feature]!).then(e => { config.features[feature] = e; element.replaceChildren(markdown(getText(config.features[feature]!.description), undefined, { tags: { a: preview } })); }).catch(e => {}); }, contextmenu: (e) => { e.preventDefault(); const context = contextmenu(e.clientX, e.clientY, [ dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); const _feature: Feature = { id: getID(), description: getID(), effect: [] }; config.features[_feature.id] = _feature; config.peoples[people]!.options[level]!.push(_feature.id); element.parentElement?.appendChild(render(people, level, _feature.id)); } } }, [ text('Nouveau') ]), config.peoples[people]!.options[level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { config.peoples[people]!.options[level] = config.peoples[people]!.options[level].filter(e => e !== feature); delete config.features[feature]; element.remove(); } }) } } }, [ text('Supprimer') ]) : undefined, ], { placement: "right-start", priority: false }); }}}, [ markdown(getText(config.features[feature]!.description), undefined, { tags: { a: preview } }) ]); return element; } const peopleRender = (people: RaceConfig) => { return foldable(() => Object.entries(people.options).flatMap(level => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]), div("flex flex-row gap-4 justify-center", level[1].map((option) => render(people.id, parseInt(level[0], 10) as Level, option))), ]), [ input('text', { defaultValue: people.name, input: (value) => { people.name = value }, class: 'w-32' }), input('text', { defaultValue: people.description, input: (value) => { people.description = value }, class: 'w-full' }) ], { class: { container: 'gap-2 max-h-full', title: 'flex flex-row', content: 'flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8' }, open: false }) } const container = div('flex flex-col gap-2', Object.values(config.peoples).map(peopleRender)); const content = [ div('flex flex-col py-2 gap-2', [ div('w-full flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), container ]) ]; return content; } training() { let tab = 0; const switchTab = (tab: number) => { tab = tab; _statIndicator.setAttribute('data-text', mainStatTexts[MAIN_STATS[tab] as MainStat]); _statIndicator.style.left = `${tab * 1.5}em`; _statContainer.style.left = `-${tab * 100}%`; } const render = (stat: MainStat, level: TrainingLevel, feature: string) => { let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => { FeaturePanel.edit(config.features[feature]!).then(e => { config.features[feature] = e; element.replaceChildren(markdown(getText(config.features[feature]!.description), undefined, { tags: { a: preview } })); }).catch(e => {}); }, contextmenu: (e) => { e.preventDefault(); const context = contextmenu(e.clientX, e.clientY, [ dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); const _feature: Feature = { id: getID(), description: getID(), effect: [] }; config.features[_feature.id] = _feature; config.training[stat][level].push(_feature.id); element.parentElement?.appendChild(render(stat, level, _feature.id)); } } }, [ text('Nouveau') ]), config.training[stat][level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => { context.close(); confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) { config.training[stat][level as any as TrainingLevel] = config.training[stat][level as any as TrainingLevel].filter(e => e !== feature); delete config.features[feature]; element.remove(); } }) } } }, [ text('Supprimer') ]) : undefined, ], { placement: "right-start", priority: false }); }}}, [ markdown(getText(config.features[feature]!.description), undefined, { tags: { a: preview } }) ]); return element; }; const statRenderBlock = (stat: MainStat) => { return Object.entries(config.training[stat]).map( (level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]), div("flex flex-row gap-4 justify-center", level[1].map((option) => render(stat, parseInt(level[0], 10) as TrainingLevel, option))), ]) } const _options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record); const _statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' }); const _statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(_options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e])))); const content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20", [ div('flex flex-shrink gap-3 items-center relative w-48 ms-12', [ ...MAIN_STATS.map((e, i) => dom('span', { listeners: { click: () => switchTab(i) }, class: 'block w-2.5 h-2.5 m-px outline outline-1 outline-transparent hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer' })), _statIndicator, ]), ]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ _statContainer ])]; switchTab(0); return content; } aspects() { const render = (aspect: AspectConfig) => { return { name: input('text', { input: (value) => { aspect.name = value }, defaultValue: aspect.name, class: '!m-0 w-full' }), description: input('text', { input: (value) => { aspect.description = value }, defaultValue: aspect.description, class: '!m-0 w-full' }), stat: select(MAIN_STATS.map(f => ({ text: mainStatTexts[f], value: f })), { change: (value) => aspect.stat = value, defaultValue: aspect.stat, class: { container: '!m-0 w-full' } }), alignment: select(ALIGNMENTS.map(f => ({ text: alignmentTexts[f], value: f })), { change: (value) => aspect.alignment = value, defaultValue: aspect.alignment, class: { container: '!m-0 w-full' } }), magic: toggle({ defaultValue: aspect.magic, change: (value) => aspect.magic = value, class: { container: '' } }), difficulty: numberpicker({ min: 6, max: 13, input: (value) => aspect.difficulty = value, defaultValue: aspect.difficulty, class: '!m-0 w-full' }), physic: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.physic.min, input: (value) => aspect.physic.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.physic.max, input: (value) => aspect.physic.max = value, class: '!m-0' }) ]), mental: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.mental.min, input: (value) => aspect.mental.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.mental.max, input: (value) => aspect.mental.max = value, class: '!m-0' }) ]), personality: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.personality.min, input: (value) => aspect.personality.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.personality.max, input: (value) => aspect.personality.max = value, class: '!m-0' }) ]), action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:file-text'), () => {}, 'p-1'), button(icon('radix-icons:trash'), () => remove(aspect), 'p-1') ]) }; } const add = () => { const id = getID(); config.aspects[id] = { id, name: '', description: '', stat: 'strength', alignment: 'loyal_good', magic: false, difficulty: 6, physic: { min: 0, max: 30 }, mental: { min: 0, max: 20 }, personality: { min: 0, max: 20 }, options: [] }; const element = redraw(); content.parentElement?.replaceChild(element, content); content = element; }; const remove = (aspect: AspectConfig) => { confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => { if(e) { delete config.aspects[aspect.id]; const element = redraw(); content.parentElement?.replaceChild(element, content); content = element; } }) } const redraw = () => table(Object.values(config.aspects).map(render), { name: 'Nom', description: 'Description', stat: 'Buff de stat', alignment: 'Alignement', magic: 'Magie', difficulty: 'Difficulté', physic: 'Physique', mental: 'Mental', personality: 'Caractère', action: 'Actions' }, { class: { table: 'flex-1' } }); let content = redraw(); return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ]; } spells() { const spellTagTexts = { 'damage': 'Dégâts', 'buff': 'Buff', 'debuff': 'Débuff', 'support': 'Support', 'tank': 'Tank', 'movement': 'Mouvement', 'utilitary': 'Utilitaire', } as Record; const editing = reactive({ id: '', }); const render = (spell: SpellConfig) => { return foldable(() => [ markdown(spell.description, undefined, { tags: { a: preview } }), ], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }), div('flex flex-1 flex-row gap-2 items-center', [ dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`Rang ${spell.rank}`) ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spellTypeTexts[spell.type]) ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${spell.cost} mana`) ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.speed === 'action' ? 'Action' : spell.speed === 'reaction' ? 'Réaction' : `${spell.speed} minutes`) ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${elementTexts[spell.elements[0]!].text}${spell.elements.length > 1 ? ` (+${spell.elements.length - 1})` : ''}`) ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.range === 'personnal' ? 'Personnel' : spell.range === 0 ? 'Toucher' : `${spell.range} cases`) ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${spell.tags && spell.tags.length > 0 ? spellTagTexts[spell.tags[0]!] : ''}${spell.tags && spell.tags.length > 1 ? ` (+${spell.tags.length - 1})` : ''}`) ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.concentration ? 'Concentration' : '') ]), ]), div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:pencil-1'), () => editing.id = spell.id, 'p-1'), button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false }); }; const edit = (id: string) => { const spell = config.spells[id] ? { ...config.spells[id] } : undefined; if(!spell) return; MarkdownEditor.singleton.onChange = v => {}; MarkdownEditor.singleton.content = spell.description; return foldable([ MarkdownEditor.singleton.dom ], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }), div('flex flex-row gap-2 items-center', [ dom('label', { class: 'flex flex-col items-center justify-start 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' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start 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' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { 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' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start 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' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start 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' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start 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' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]), ]), div('flex flex-row gap-2', [ tooltip(button(icon('radix-icons:check'), () => { spell.description = MarkdownEditor.singleton.content; Object.assign(config.spells[spell.id]!, spell); editing.id = ''; }, 'p-1'), "Valider", 'left'), tooltip(button(icon('radix-icons:cross-1'), () => { editing.id = ''; }, 'p-1'), "Annuler", 'left') ]), ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false }); }; const add = () => { const id = getID(); config.spells[id] = { id, name: '', rank: 1, type: 'precision', cost: 1, speed: 'action', elements: [], description: '', concentration: false, range: 0, tags: [], }; }; const remove = (spell: SpellConfig) => { confirm('Voulez vous vraiment supprimer ce sort ?').then(e => { if(e) { delete config.spells[spell.id]; } }); } return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), div('flex flex-col divide-y', { list: () => Object.values(config.spells), render: (e, _c) => editing.id === e.id ? edit(e.id) : render(e)}) ] ) ]; } actions() { let editing: { type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string } | undefined; const render = (type: 'action' | 'reaction' | 'freeaction' | 'passive', feature: { id: string, name: string, description: string, cost?: number }) => { const md = markdownReference(getText(feature.description), undefined, { tags: { a: preview }, class: 'ms-2 px-2 py-1 border-l-4 border-light-30 dark:border-dark-30' }); const buttons = div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:pencil-1'), () => edit(type, feature.id), 'p-1'), 'Modifier', 'left'), tooltip(button(icon('radix-icons:trash'), () => remove(type, feature.id), 'p-1'), 'Supprimer', 'right') ]); return { dom: div('flex flex-col gap-2', [ div('flex flex-row justify-between', [ input('text', { defaultValue: feature.name, input: value => { feature.name = value }, placeholder: 'Nom', class: '!mx-0 w-80' }), div('flex flex-row gap-2 items-center', [ type === 'action' || type === 'reaction' ? div('flex flex-row items-center', [ numberpicker({ defaultValue: feature?.cost ?? 0, input: value => feature.cost = value, class: '!mx-1', max: type === 'action' ? 3 : 2, min: 0 }), text(`point${(feature?.cost ?? 0) > 1 ? 's' : ''}`)]) : undefined, buttons ])]), md.current, ]), buttons, md, type, id: feature.id, }; } const add = (type: 'action' | 'reaction' | 'freeaction' | 'passive') => { const feature: { id: string, name: string, description: string, cost?: number } = { id: getID(), name: '', description: getID(), // i18nID cost: type === 'action' || type === 'reaction' ? 1 : undefined, } config.texts[feature.description] = { 'fr_FR': '' }; config[type][feature.id] = feature; const option = render(type, feature); options.push(option); optionHolder.appendChild(option.dom); }; const remove = (type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string) => { const feature = config[type][id]!; confirm(`Voulez vous vraiment supprimer l'effet "${feature.name}" ?`).then(e => { if(e) { delete config.texts[feature.description]; delete config[type][id]; const idx = options.findIndex(e => e.type === type && e.id === id); options.splice(idx, 1)[0]?.dom.remove(); } }); }; const edit = (type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string) => { const feature = config[type][id]!; const option = options.find(e => e.type === type && e.id === id); if(editing) { const idx = options.findIndex(e => e.id === editing!.id && e.type === editing!.type); const rerender = render(editing.type, config[editing.type][editing.id]!); options[idx]?.dom.replaceWith(rerender.dom); options[idx] = rerender; } editing = { id, type }; const buttons = div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:check'), () => { setText(feature.description, editor.content); const rerender = render(type, feature); option!.buttons.replaceWith(rerender.buttons); option!.buttons = rerender.buttons; option!.md.current.replaceWith(rerender.md.current); option!.md = rerender.md; editing = undefined; }, 'p-1'), 'Valider', 'left'), tooltip(button(icon('radix-icons:cross-1'), () => { const rerender = render(type, feature); option!.buttons.replaceWith(rerender.buttons); option!.buttons = rerender.buttons; option!.md.current.replaceWith(rerender.md.current); option!.md = rerender.md; editing = undefined; }, 'p-1'), 'Rejeter', 'right') ]); option!.buttons.replaceWith(buttons); option!.buttons = buttons; const editor = MarkdownEditor.singleton; editor.content = getText(feature.description); editor.onChange = (value) => {}; const editorDom = div('p-1 border border-light-35 dark:border-dark-35', [ editor.dom ]); option!.md.current.replaceWith(editorDom); option!.md.current = editorDom; } const options = [...Object.values(config.action).map(e => render('action', e)), ...Object.values(config.reaction).map(e => render('reaction', e)), ...Object.values(config.freeaction).map(e => render('freeaction', e)), ...Object.values(config.passive).map(e => render('passive', e))]; const optionHolder = div('flex flex-col gap-4', options.map(e => e.dom)); return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), optionmenu([{ title: 'Action', click: () => add('action') }, { title: 'Réaction', click: () => add('reaction') }, { title: 'Action libre', click: () => add('freeaction') }, { title: 'Passif', click: () => add('passive') }], { position: 'left-start' }), 'p-1') ]), optionHolder ] ) ]; } items() { const defaultItem = (category: Category): ItemConfig => { const common: CommonItemConfig = { id: getID(), name: '', description: getID(), // i18nID flavoring: getID(), // i18nID rarity: 'common', equippable: false, consummable: false, }; switch(category) { case 'armor': return { ...common, category: category, health: 0, absorb: { percent: 0, static: 0 }, type: 'light' } as CommonItemConfig & ArmorConfig; case 'weapon': return { ...common, category: category, damage: { type: 'slashing', value: '0' }, type: ['classic'] } as CommonItemConfig & WeaponConfig; case 'wondrous': case 'mundane': return { ...common, category: category } as CommonItemConfig & (MundaneConfig | WondrousConfig); } }; const add = (category: Category) => { const item = defaultItem(category); setText(item.description, ''); config.items[item.id!] = item; }; const remove = (item: ItemConfig) => { confirm(`Voulez vous vraiment supprimer l'effet "${item.name}" ?`).then(e => { if(e) { delete config.texts[item.description]; delete config.items[item.id]; } }); }; const edit = (item: ItemConfig) => { ItemPanel.edit(item).then(f => { Object.assign(config.items[f.id]!, f); }).catch((e) => {}); } return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), optionmenu([{ title: 'Objet inerte', click: () => add('mundane') }, { title: 'Armure', click: () => add('armor') }, { title: 'Arme', click: () => add('weapon') }, { title: 'Objet magique', click: () => add('wondrous') }], { position: 'left-start' }), 'p-1') ]), div('flex flex-col gap-1', { list: () => Object.keys(config.items), render: (e, _c) => { const item = config.items[e]; if(!item) return; return _c ?? foldable(() => [ markdown(getText(item.description)) ], [div('flex flex-row justify-between', [ div('flex flex-row items-center gap-4', [ div('flex flex-row items-center gap-4', [ span(() => [colorByRarity[item.rarity], 'text-lg'], () => item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => (() => span('', e)))) ]), ]), div('flex flex-row gap-1', [ div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [ div('flex flex-row w-20 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.powercost || item.capacity ? `${item.powercost ?? 0}/${item.capacity ?? 0}` : '-') ]), div('flex flex-row w-20 gap-2 justify-between items-center px-2', [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.weight?.toString() ?? '-') ]), div('flex flex-row w-20 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.charge ? `${item.charge}` : '-') ]), div('flex flex-row w-20 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => item.price ? `${item.price}` : '-') ]), ]), button(icon('radix-icons:pencil-2'), () => edit(config.items[e]!), 'p-1'), button(icon('radix-icons:trash'), () => remove(config.items[e]!), 'p-1'), ]) ])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } }) } }) ] ) ]; } trees() { const add = () => { const description = getID(); setText(description, ""); const id = getID(); config.features[id] = { id, description, effect: [], }; config.trees[editing.tree!]!.nodes[id] = { id }; } const editing = reactive({ tree: undefined as string | undefined }); return [div('', [ () => editing.tree !== undefined ? undefined : div('flex flex-row gap-1 justify-start overflow-x-auto max-w-full', { list: Object.keys(config.trees), render: (e, _c) => _c ?? div('grid grid-cols-2 gap-2 items-baseline w-64 border border-light-35 dark:border-dark-35 p-2', [ span('text-lg font-semibold tracking-thigh', config.trees[e]!.name), div('flex flex-row justify-end', [ tooltip(button(icon('radix-icons:pencil-1', { width: 16, height: 16 }), () => editing.tree = e, 'p-1'), 'Modifier', 'left') ]), span('italic', `${Object.keys(config.trees[e]!.nodes).length} nodes`) ]) }), () => editing.tree === undefined ? undefined : div('', [ foldable([ div('flex flex-col gap-2', { list: Object.keys(config.trees[editing.tree]!.nodes), render: (e, _c) => _c ?? dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: () => { FeaturePanel.edit(config.features[e]!).then(feature => { config.features[e] = feature; }).catch(e => {}); }}}, [ markdown(getText(config.features[e]!.description), undefined, { tags: { a: preview } }) ]) }) ], [ span('text-lg font-bold px-2', 'Nodes'), button(text('Nouvelle node'), () => add(), 'py-1 px-2') ], { open: true, class: { title: 'flex flex-row justify-between' } }) ]), ])]; } private save() { navigator.clipboard.writeText(JSON.stringify(config)); } } type FeatureOption = Partial & { id: string }; class FeatureEditor { private _list: Record | FeatureOption[]; private _id: string; private _draft: boolean; private _arr: boolean; private option!: FeatureOption; container!: HTMLElement; constructor(list: Record | FeatureOption[], id: string, draft: boolean) { this._arr = Array.isArray(list); this._list = list; this._id = id; if(this._arr ? !(list as FeatureOption[]).find(e => e.id === id) : !list.hasOwnProperty(id)) throw new Error(); this.read(); this._draft = draft; this.container = div(); if(draft) this.edit(); else this.show(); } private read() { this.option = JSON.parse(JSON.stringify(this._arr ? (this._list as FeatureOption[]).find(e => e.id === this._id)! : (this._list as Record)[this._id]!)); } private update() { if(this._arr) { const idx = (this._list as FeatureOption[]).findIndex(e => e.id === this.option.id); if(idx === -1) throw new Error(); (this._list as FeatureOption[])[idx]! = this.option; } else { (this._list as Record)[this.option.id] = this.option; } } private delete() { if(this._arr) { const idx = (this._list as FeatureOption[]).findIndex(e => e.id === this.option.id); if(idx === -1) throw new Error(); (this._list as FeatureOption[]).splice(idx, 1); } else { delete (this._list as Record)[this.option.id]; } } private show() { const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-center', [ div('px-4 flex items-center h-full', [ markdown(textFromEffect(this.option), undefined, { tags: { a: preview } }) ]), div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => this.edit(), '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.delete(); this.container.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") ]) ]) ]); this.container.replaceWith(content); this.container = content; } private static match(effect: FeatureOption): Partial | undefined { switch(effect.category) { case 'value': return flattenFeatureChoices.find(e => e.category === 'value' && e.property === effect.property); case 'choice': return flattenFeatureChoices.findLast(e => e.category === 'choice'); case 'tree': return flattenFeatureChoices.findLast(e => e.category === 'tree'); case 'list': return flattenFeatureChoices.find(e => e.category === 'list' && e.list === effect.list); } } private editByCategory(buffer: FeatureOption) { let top: NodeChildren = [], bottom: NodeChildren = []; switch(buffer.category) { case 'value': return this.editValue(buffer as Partial); case 'list': return this.editList(buffer as Partial); case 'choice': return this.editChoice(buffer as Partial); case 'tree': return this.editTree(buffer as Partial); default: break; } return { top, bottom }; } private editValue(buffer: Partial) { const valueVariable = () => typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { buffer.value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { buffer.value = value; summaryText.textContent = textFromEffect(buffer); } }); const summaryText = text(textFromEffect(buffer)); let valueSelection = valueVariable(); return { top: [ select([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].includes(buffer.property ?? '') || ['defense/'].some(e => buffer.property?.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { buffer.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.value = (typeof buffer.value === 'number' ? '' as any as false : 0); const newValueSelection = valueVariable(); valueSelection.replaceWith(newValueSelection); valueSelection = newValueSelection; summaryText.textContent = textFromEffect(buffer); }, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Changer d\'editeur', 'bottom'), ], bottom: [ div('px-2 py-1 flex items-center flex-1', [summaryText]) ] }; } private editList(buffer: Partial) { let list: Option[] = []; switch(buffer.list) { case undefined: break; case "spells": list = Object.values(config.spells).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(e.description)) ]) ]), value: e.id })); break; case "mastery": list = Object.entries(masteryTexts).map(e => ({ text: e[1].text, value: e[0] })); break; default: list = Object.values(config[buffer.list]).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id })); break; } return { top: [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => { buffer.action = value as 'add' | 'remove'; this.edit(); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ], bottom: [ combobox(list, { defaultValue: buffer.item, change: (item) => buffer.item = 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' }, fill: 'contain' }) ] } } private editTree(buffer: Partial) { const path = input("text", { defaultValue: buffer.option ?? "", input: v => { buffer.option = v }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[240px]' }); const edit = tooltip(button(icon('radix-icons:gear', { width: 16, height: 16 }), function() { path.value = ""; this.parentNode?.insertBefore(path, this); this.replaceWith(remove); }, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Option fixe', 'right'), remove = tooltip(button(icon('radix-icons:cross-2', { width: 16, height: 16 }), function() { delete buffer.option; path.value = ""; path.remove(); this.replaceWith(edit); }, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Option fixe', 'right'); return { top: [ combobox(Object.values(config.trees).map(e => ({ text: e.name, value: e.name })), { defaultValue: buffer.tree, change: v => buffer.tree = v, class: { container: 'bg-light-25 dark:bg-dark-25 w-48 -m-px hover:z-10 h-[36px]' }, fill: 'cover' }), ...(buffer.option === undefined ? [edit] : [path, remove]) ], bottom: [ ], } } private editChoice(buffer: Partial) { const availableChoices: Option>[] = 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>[]; const addChoice = () => { const choice: { text: string; effects: (Partial)[]; } = { effects: [{ id: getID() }], text: '' }; buffer.options ??= []; buffer.options.push(choice as FeatureChoice["options"][number]); return choice; }; const addEffect = (choice: { text: string; effects: (Partial)[] }) => { const effect: (Partial) = { id: getID() }; choice.effects.push(effect); return effect; }; const renderEffect = (option: { text: string; effects: (Partial)[] }, effect: Partial) => { const { top: _top, bottom: _bottom } = this.editByCategory(effect as FeatureOption); let element = div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [ div('flex flex-row flex-1', [ combobox(availableChoices, { defaultValue: FeatureEditor.match(effect as FeatureOption) as Partial | undefined, class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e: Partial) => { const idx = option.effects.findIndex(e => e === effect); option.effects[idx] = effect = { ...e, id: effect.id }; const _element = renderEffect(option, effect); element.replaceWith(_element); element = _element; } }), ..._top, ]), div('flex', [ tooltip(button(icon('radix-icons:trash'), () => { option.effects = option.effects.filter(e => e === effect); element.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") ]) ]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', _bottom) ]); return element; } const renderOption = (option: { text: string; effects: (Partial)[] }, state: boolean) => { const effects = div('flex flex-col -m-px flex flex-col ms-px ps-8 w-full', option.effects.map(e => renderEffect(option, e))), effectLength = text(option.effects.length); let _content = foldable([ effects ], [ div('flex flex-row flex-1 justify-between', [ div('flex flex-row items-center', [ input('text', { defaultValue: option.text, input: (value) => { option.text = value }, placeholder: 'Nom de l\'option', class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1' }), span('italic ps-8 pe-2', 'Effets: '), span('font-bold', effectLength) ]), div('flex flex-row flex-shrink-1', [ tooltip(button(icon('radix-icons:plus'), () => effects.appendChild(renderEffect(option, addEffect(option))), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvel effet', 'bottom'), , tooltip(button(icon('radix-icons:trash'), () => { _content.remove(); buffer.options?.splice(buffer.options.findIndex(e => e !== option), 1); }, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' }, open: state }); return _content; } const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => renderOption(e, false)) ?? []); return { top: [ input('text', { defaultValue: buffer.text, input: (value) => { (buffer as FeatureChoice).text = value }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => list.appendChild(renderOption(addChoice(), true)), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvelle option', 'bottom') ], bottom: [ list ], } } private edit() { const redraw = () => { const { top, bottom } = this.editByCategory(this.option); return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [ div('flex flex-row flex-1', [ combobox(featureChoices, { defaultValue: FeatureEditor.match(this.option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => { this.option = { id: this.option.id, ...e } as FeatureOption; content = redraw(); this.container.replaceWith(content); this.container = content; } }), ...top, ]), div('flex', [ tooltip(button(icon('radix-icons:check'), () => { this.update(); this.read(); this.show(); this._draft = false; }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), () => { if(this._draft) { this.delete(); this.container.remove(); } else { this.read(); this.show(); } }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ]) ]), bottom.length > 0 ? div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', bottom) : undefined ]); } let content = redraw(); this.container.replaceWith(content); this.container = content; } } export class FeaturePanel { static render(feature: Feature, success: (feature: Feature) => void, failure: (feature: Feature) => void) { const _feature = JSON.parse(JSON.stringify(feature)) as Feature; const effectContainer = div('grid grid-cols-2 gap-4 px-2', _feature.effect.map(e => new FeatureEditor(_feature.effect!, e.id, false).container)); MarkdownEditor.singleton.content = getText(_feature.description); MarkdownEditor.singleton.onChange = (value) => setText(_feature.description, value); return dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [ div('flex flex-row justify-between items-center', [ tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => { success!(_feature); MarkdownEditor.singleton.onChange = undefined; }, 'p-1'), 'Valider', 'left'), dom('label', { class: 'flex justify-center items-center my-2' }, [ dom('span', { class: 'pb-1 md:p-0', text: "ID" }), input("text", { defaultValue: _feature.id, disabled: true, class: `mx-4 text-light-70 dark:text-dark-70 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-25 dark:bg-dark-25 border-light-30 dark:border-dark-30` }) ]), tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => { failure!(feature); MarkdownEditor.singleton.onChange = undefined; }, 'p-1'), 'Annuler', 'left'), ]), dom('span', { class: 'flex flex-col justify-start items-start my-2 gap-4' }, [ div('flex w-full items-center justify-between', [ dom('span', { class: 'pb-1 md:p-0', text: "Description" }), tooltip(button(icon('radix-icons:clipboard', { width: 20, height: 20 }), () => { MarkdownEditor.singleton.content = _feature?.effect.map(e => textFromEffect(e)).join('\n') ?? _feature?.description ?? MarkdownEditor.singleton.content; setText(_feature.description, MarkdownEditor.singleton.content); }, 'p-1'), 'Description automatique', 'left'), ]), 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]', [ MarkdownEditor.singleton.dom ]), ]), div('flex flex-col gap-2 w-full', [ div('flex flex-row justify-between', [ dom('h3', { class: 'text-lg font-bold', text: 'Effets' }), tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => { const f = { id: getID(), }; //@ts-expect-error _feature.effect.push(f); effectContainer.appendChild(new FeatureEditor(_feature.effect, f.id, true).container); }, 'p-1'), 'Ajouter', 'left'), ]), effectContainer, ]) ]); } static edit(feature: Feature): Promise { let container: HTMLElement, close: Function; return new Promise((success, failure) => { container = FeaturePanel.render(feature, success, failure); close = fullblocker([container], { priority: true, closeWhenOutside: false, }).close; setTimeout(() => container.setAttribute('data-state', 'active'), 1); }).finally(() => { setTimeout(close, 150); container.setAttribute('data-state', 'inactive'); }); } } export class ItemPanel { static descriptionEditor: MarkdownEditor = new MarkdownEditor(); static flavoringEditor: MarkdownEditor = new MarkdownEditor(); static render(item: ItemConfig, success: (item: ItemConfig) => void, failure: (item: ItemConfig) => void) { const _item = JSON.parse(JSON.stringify(item)) as ItemConfig; ItemPanel.descriptionEditor.content = getText(_item.description); ItemPanel.descriptionEditor.onChange = (value) => setText(_item.description, value); ItemPanel.flavoringEditor.content = getText(_item.flavoring); ItemPanel.flavoringEditor.onChange = (value) => { if(!_item.flavoring) { _item.flavoring = getID(); } setText(_item.flavoring, value); }; const effectContainer = div('grid grid-cols-2 gap-4 px-2 flex-1', _item.effects?.map(e => new FeatureEditor(_item.effects!, e.id, false).container)); return dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [ div('flex flex-row justify-between items-center', [ tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => { success!(_item); ItemPanel.descriptionEditor.onChange = undefined; ItemPanel.flavoringEditor.onChange = undefined; }, 'p-1'), 'Valider', 'left'), dom('label', { class: 'flex justify-center items-center my-2' }, [ dom('span', { class: 'pb-1 md:p-0', text: "Nom" }), input('text', { defaultValue: _item.name, input: (v) => { _item.name = v }, class: 'w-96' }) ]), tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => { failure!(item); ItemPanel.descriptionEditor.onChange = undefined; ItemPanel.flavoringEditor.onChange = undefined; }, 'p-1'), 'Annuler', 'left'), ]), foldable([ div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.weight !== undefined, change: function(value) { _item.weight = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Poids'), ]), numberpicker({ defaultValue: _item.weight, disabled: _item.weight === undefined, input: (v) => _item.weight = v, class: '!w-1/3' }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.price !== undefined, change: function(value) { _item.price = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Prix'), ]), numberpicker({ defaultValue: _item.price, disabled: _item.price === undefined, input: (v) => _item.price = v, class: '!w-1/3' }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.capacity !== undefined, change: function(value) { _item.capacity = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Capacité magique'), ]), numberpicker({ defaultValue: _item.capacity, disabled: _item.capacity === undefined, input: (v) => _item.capacity = v, class: '!w-1/3' }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.powercost !== undefined, change: function(value) { _item.powercost = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Puissance magique'), ]), numberpicker({ defaultValue: _item.powercost, disabled: _item.powercost === undefined, input: (v) => _item.powercost = v, class: '!w-1/3' }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.equippable, change: function(value) { _item.equippable = value; this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Equipable'), ]), div('flex flex-row gap-2 items-center mx-4', [ checkbox({ defaultValue: _item.consummable, change: function(value) { _item.consummable = value; this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Consommable'), ]) ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.charge !== undefined, change: function(value) { _item.charge = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Charges'), ]), numberpicker({ defaultValue: _item.charge, disabled: _item.charge === undefined, input: (v) => _item.charge = v, class: '!w-1/3' }), ]), ], [ 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: _item.rarity, change: (v) => _item.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 } ), _item.category === 'armor' ? foldable([ div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Type'), ]), select<'light' | 'medium' | 'heavy'>([{ text: 'Armure légère', value: 'light' }, { text: 'Armure moyenne', value: 'medium' }, { text: 'Armure lourde', value: 'heavy' }], { defaultValue: _item.type, change: (v) => _item.type = v, class: { container: '!w-1/2' } }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Durabilité'), ]), numberpicker({ defaultValue: _item.health, input: (v) => _item.health = v, class: '!w-1/3' }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Absorption (fixe)'), ]), numberpicker({ defaultValue: _item.absorb.static, input: (v) => _item.absorb.static = v, class: '!w-1/3' }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Absorption (%)'), ]), numberpicker({ defaultValue: _item.absorb.percent, input: (v) => _item.absorb.percent = v, class: '!w-1/3' }), ]), ], [ span('text-lg font-bold', "Armure") ], { 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 } ) : undefined, _item.category === 'weapon' ? foldable([ div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Type de dégâts'), ]), select(Object.keys(damageTypeTexts).map(e => ({ text: damageTypeTexts[e as DamageType], value: e as DamageType })), { defaultValue: _item.damage.type, change: (v) => _item.damage.type = v, class: { container: '!w-1/3' } }), ]), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Dégats'), ]), input('text', { defaultValue: _item.damage.value, input: (v) => { _item.damage.value = v }, class: '!w-1/3' }), ]), ], [ 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('', 'Categorie d\'arme'), ]), multiselect(Object.keys(weaponTypeTexts).map(e => ({ text: weaponTypeTexts[e as WeaponType], value: e as WeaponType })), { defaultValue: _item.type, change: (v) => _item.type = 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 } ) : undefined, 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]', [ ItemPanel.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, }), 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]', [ ItemPanel.flavoringEditor.dom ])], [ span('text-lg font-bold px-2', "Lore") ], { class: { container: 'gap-4 pb-2 border-b border-light-35 dark:border-dark-35' }, open: true, }), 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(), }; _item.effects ??= []; _item.effects.push(f as any as FeatureValue | FeatureEquipment | FeatureList); effectContainer.appendChild(new FeatureEditor(_item.effects, f.id, true).container); }, 'p-1 hidden group-data-[active]:block'), 'Ajouter', 'left'), ], { class: { container: 'flex flex-col gap-2 w-full', title: 'flex flex-row justify-between px-2' } }) ]); } static edit(item: ItemConfig): Promise { let container: HTMLElement, close: Function; return new Promise((success, failure) => { container = ItemPanel.render(item, success, failure); close = fullblocker([container], { priority: true, closeWhenOutside: false, }).close; setTimeout(() => container.setAttribute('data-state', 'active'), 1); }).finally(() => { setTimeout(close, 150); container.setAttribute('data-state', 'inactive'); }); } } const featureChoices: Option>[] = [ { 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 inné', value: { category: 'list', list: 'spells', action: 'add' }, }, { text: 'Spécialisation', value: { category: 'value', property: 'spec', operation: 'add', value: 1 }, }, { text: 'Objets', value: [ { text: 'Puissance magique', value: { category: 'value', property: 'itempower', operation: 'add', value: 1 }, }, { text: 'Rareté fabricable', value: { category: 'value', property: 'craft/level', operation: 'add', value: 1 }, }, { text: 'Bonus de fabrication', value: { category: 'value', property: 'craft/bonus', operation: 'add', value: 1 }, }, ] }, { text: 'Défense', value: [ { 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: '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>[], { 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: [ { text: 'Mod. de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } }, { text: 'Mod. de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } }, { text: 'Mod. de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } }, { text: 'Mod. d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } }, { text: 'Mod. de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } }, { text: 'Mod. de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } }, { text: 'Mod. de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } }, { text: 'Mod. au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [ { text: 'Mod. de force', effects: [ { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } ] }, { text: 'Mod. de dextérité', effects: [ { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } ] }, { text: 'Mod. de constitution', effects: [ { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } ] }, { text: 'Mod. d\'intelligence', effects: [ { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } ] }, { text: 'Mod. de curiosité', effects: [ { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } ] }, { text: 'Mod. de charisme', effects: [ { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } ] }, { text: 'Mod. de psyché', effects: [ { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } ] } ]} as Partial} ] }, { text: 'Jet de résistance', value: [ { text: 'Résistance > Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } }, { text: 'Résistance > Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } }, { text: 'Résistance > Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 } }, { text: 'Résistance > Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 } }, { text: 'Résistance > Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 } }, { 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 }] } ]} as Partial} ] }, { text: 'Bonus à l\'attaque', value: RESISTANCES.map(e => ({ text: `Bonus > ${resistanceTexts[e]}`, value: { category: 'value', property: `bonus/resistance/${e}`, operation: 'add', value: 1 } })) }, { text: 'Magie', value: [ { text: 'Rang', value: [ { text: 'Rang > Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } }, { text: 'Rang > Sorts de savoir', value: { category: 'value', property: 'spellranks/knowledge', operation: 'add', value: 1 } }, { text: 'Rang > Sorts d\'instinct', value: { category: 'value', property: 'spellranks/instinct', operation: 'add', value: 1 } }, { text: 'Rang > Œuvres', value: { category: 'value', property: 'spellranks/arts', operation: 'add', value: 1 } }, ] }, { text: 'Bonus par type', value: [ { text: 'Bonus > Précision', value: { category: 'value', property: 'bonus/spells/type/precision', operation: 'add', value: 1 } }, { 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 > Œuvres', value: { category: 'value', property: 'bonus/spells/type/arts', operation: 'add', value: 1 } }, ] }, { text: 'Bonus par rang', value: [ { text: 'Bonus > Sorts de rang 1', value: { category: 'value', property: 'bonus/spells/rank/1', operation: 'add', value: 1 } }, { text: 'Bonus > Sorts de rang 2', value: { category: 'value', property: 'bonus/spells/rank/2', operation: 'add', value: 1 } }, { text: 'Bonus > Sorts de rang 3', value: { category: 'value', property: 'bonus/spells/rank/3', operation: 'add', value: 1 } }, { text: 'Bonus > Sorts uniques', value: { category: 'value', property: 'bonus/spells/rank/4', operation: 'add', value: 1 } }, ] }, { text: 'Bonus par element', value: SPELL_ELEMENTS.map(e => ({ text: `Bonus > ${elementTexts[e].text}`, value: { category: 'value', property: `bonus/spells/elements/${e}`, operation: 'add', value: 1 } })) }, ] }, { text: 'Aspect', value: [ { text: 'Aspect > Durée', value: { category: 'value', property: 'aspect/duration', operation: 'add', value: 15 } }, { text: 'Aspect > Nombre', value: { category: 'value', property: 'aspect/amount', operation: 'add', value: 1 } }, { text: 'Aspect > Bonus au jet', value: { category: 'value', property: 'aspect/bonus', operation: 'add', value: 1 } }, { text: 'Aspect > Tier', value: { category: 'value', property: 'aspect/tier', 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' }, }, { text: 'Passif', value: { category: 'list', list: 'passive', action: 'add' }, }, { text: 'Choix', value: { category: 'choice', text: '', options: [] }, }, ]; const flattenFeatureChoices = Tree.accumulate(featureChoices, 'value', (item) => Array.isArray(item.value) ? undefined : item.value).filter(e => !!e) as Partial[]; function textFromEffect(effect: Partial): string { if(effect.category === 'value') { if(effect.property === undefined) return ''; switch(effect.property) { case 'health': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' PV max.' } }) : textFromValue(effect.value, { prefix: { truely: 'PV max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (PV = interdit).' }); case 'mana': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' mana max.' } }) : textFromValue(effect.value, { prefix: { truely: 'Mana max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Mana = interdit).' }); case 'spellslots': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' sort(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Sorts maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Sorts = interdit).' }); case 'artslots': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' œuvre(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Œuvres maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Œuvres = interdit).' }); case 'speed': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' case(s) de course.' }, falsely: '+0 cases de course' }) : textFromValue(effect.value, { prefix: { truely: 'Vitesse de course de ' }, suffix: { truely: ' case(s).' }, falsely: 'Déplacement impossible.' }); case 'capacity': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' unité(s) d\'quipement.' } }) : textFromValue(effect.value, { prefix: { truely: 'Capacité d\'équipement fixé à ' }, suffix: { truely: ' unité(s).' }, falsely: 'Impossible de posséder du materiel.' }); case 'initiative': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' à l\'itiniative.' } }) : textFromValue(effect.value, { prefix: { truely: 'Initiative fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Initiative = interdit).' }); case 'training': 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 '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; } const splited = effect.property.split('/'); switch(splited[0]) { case 'spellranks': switch(splited[1]) { case 'precision': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang(s) 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(s) 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(s) 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(s) d\'œuvres.' } }) : `Opération interdite (Rang d\'œuvres fixe).`; default: return 'Type de sort inconnu.'; } case 'aspect': switch(splited[1]) { case 'bonus': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' au jet de transformation.' } }) : `Opération interdite (Bonus de transformation fixe).`; case 'duration': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' minute(s) de temps de transformation.' } }) : `Opération interdite (Durée de transformation fixe).`; case 'amount': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' transformation(s) par jour.' } }) : `Opération interdite (Nombre de transformation fixe).`; case 'tier': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' tier(s) de transformation.' } }) : `Opération interdite (Rang d\'œuvres fixe).`; default: return 'Type de sort inconnu.'; } case 'defense': switch(splited[1]) { case 'hardcap': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Défense max ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Défense max fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Hardcap = interdit).' }); case 'static': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Base de défense ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Base de défense fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Static = interdit).' }); case 'activeparry': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active parry = interdit).' }); case 'activedodge': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active dodge = interdit).' }); case 'passiveparry': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive parry = interdit).' }); case 'passivedodge': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive dodge = interdit).' }); default: return 'Défense inconnue.'; } case 'bonus': switch(splited[1]) { case 'defense': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ` aux jets de résistance de ${mainStatTexts[splited[2] as MainStat]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Jets de résistance de ${mainStatTexts[splited[2] as MainStat]} = ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Résistance ${mainStatTexts[splited[2] as MainStat]} = interdit).` }); case 'abilities': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` }); default: return 'Bonus inconnu'; } case 'resistance': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${resistanceTexts[splited[1] as Resistance]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${resistanceTexts[splited[1] as Resistance]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (${resistanceTexts[splited[1] as Resistance]} = interdit).` }); case 'abilities': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${abilityTexts[splited[1] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${abilityTexts[splited[1] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${abilityTexts[splited[1] as Ability]}.` }); case 'modifier': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+' }, suffix: { truely: ` au mod. de ${mainStatTexts[splited[1] as MainStat]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Mod. de ${mainStatTexts[splited[1] as MainStat]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Mod. de ${mainStatShortTexts[splited[1] as MainStat]} = interdit).` }); default: break; } return `Inconnu ("${effect.property}")`; } else if(effect.category === 'list') { switch(effect.list) { case 'action': return effect.action === 'add' ? `Gain de l'action "${effect.item ? (config.action[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression de l'action "${effect.item ? (config.action[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`; case 'reaction': return effect.action === 'add' ? `Gain de la réaction "${effect.item ? (config.reaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression de la réaction "${effect.item ? (config.reaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`; case 'freeaction': return effect.action === 'add' ? `Gain de l'action libre "${effect.item ? (config.freeaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression de l'action libre "${effect.item ? (config.freeaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`; case 'passive': return effect.action === 'add' ? `Gain du passif "${effect.item ? (config.passive[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression du passif "${effect.item ? (config.passive[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`; case 'spells': return effect.action === 'add' ? `Maitrise du sort "${effect.item ? (config.spells[effect.item]?.name ?? 'Sort inconnu') : 'Sort inconnu'}".` : `Perte de maitrise du sort "${effect.item ? (config.passive[effect.item]?.name ?? 'Sort inconnu') : 'Sort inconnu'}".`; case 'sickness': return effect.action === 'add' ? `Maladie "${effect.item ? (config.sickness[effect.item]?.name ?? 'inconnue') : 'inconnue'}" permanente.` : `Maladie "${effect.item ? (config.sickness[effect.item]?.name ?? 'inconnue') : 'inconnue'}" supprimée.`; case 'mastery': return effect.action === 'add' ? `Maitrise de "${effect.item ? masteryTexts[effect.item as keyof typeof masteryTexts]?.text ?? 'inconnu' : 'inconnu'}".` : `Maitrise de "${effect.item ? masteryTexts[effect.item as keyof typeof masteryTexts]?.text ?? 'inconnu' : 'inconnu'}" supprimée.`; } } else if(effect.category === 'choice') { return `${effect.text} (${effect.options?.length ?? 0} options).`; } else if(effect.category === 'tree') { return `Progression dans l'arbre ${effect.tree && config.trees[effect.tree] ? config.trees[effect.tree]?.name : "'Inconnu'"}`; } return `Inconnu`; } function textFromValue(value?: `modifier/${MainStat}` | number | false, settings?: { prefix?: { text?: string, positive?: string, negative?: string, truely?: string }, suffix?: { text?: string, positive?: string, negative?: string, truely?: string }, falsely?: string }) { if(typeof value === 'string') return `${settings?.prefix?.truely?.replaceAll('(s)', 's') ?? ''}${settings?.prefix?.text?.replaceAll('(s)', 's') ?? ''}${mainStatShortTexts[value.split('/')[1] as MainStat] ?? 'inconnu'}${settings?.suffix?.text?.replaceAll('(s)', 's') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', 's') ?? ''}`; else if(value === false || value === undefined) return settings?.falsely ?? '0'; else if(value >= 0) return `${settings?.prefix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.prefix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}`; else return `${settings?.prefix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.prefix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}`; }