import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat } from "~/types/character"; import { div, dom, icon, text, type NodeChildren } from "./dom.util"; import { MarkdownEditor } from "./editor.util"; import { button, combobox, fakeA, input, numberpicker, select, type Option } from "./proses"; import { fullblocker, tooltip } from "./floating.util"; import { MAIN_STATS, mainStatShortTexts, mainStatTexts } from "./character.util"; import config from "#shared/character-config.json"; import { clamp, getID, ID_SIZE } from "./general.util"; import renderMarkdown from "./markdown.util"; import { Tree } from "./tree"; import markdownUtil from "./markdown.util"; const tabTexts: Record = { }; export class HomebrewBuilder { private _container: HTMLDivElement; private _content?: HTMLDivElement; private _tabsHeader: HTMLDivElement[] = []; private _tabsContent: BuilderTab[] = []; private _config: CharacterConfig; private _editor: FeatureEditor; constructor(container: HTMLDivElement) { this._config = config as CharacterConfig; this._editor = new FeatureEditor(); this._container = container; this._tabsHeader = [ dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(0) } }, [text("Peuples")]), dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(1) } }, [text("Entrainement")]), dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(2) } }, [text("Compétences")]), dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(3) } }, [text("Aspect")]), dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(4) } }, [text("Sorts")]), dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(5) } }, [text("Listes")]), ]; this._tabsContent = [ new PeopleEditor(this, this._config), new TrainingEditor(this, this._config), new AbilityEditor(this, this._config), new AspectEditor(this, this._config), /* new SpellEditor(this), new ListEditor(this), */ ]; this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto'); this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [ div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [ div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._tabsHeader), tooltip(button(icon('radix-icons:clipboard-copy', { width: 16, height: 16 }), () => this.save(), 'p-2'), 'Copier le JSON', 'left') ]), this._content, ])); this.display(1); } display(tab: number) { if(tab < 0 || tab >= this._tabsHeader.length) return; this._tabsHeader.forEach(e => e.setAttribute('data-state', 'inactive')); this._tabsHeader[tab]!.setAttribute('data-state', 'active'); this._content?.replaceChildren(...this._tabsContent[tab]!.dom); } edit(feature: Feature) { const promise = this._editor.edit(feature).then(f => { this._config.features[feature.id] = f; }).finally(() => { setTimeout(popup.close, 150); this._editor.container.setAttribute('data-state', 'inactive'); }); const popup = fullblocker([this._editor.container], { priority: true, closeWhenOutside: false, }); this._editor.container.setAttribute('data-state', 'active'); return promise; } private save() { navigator.clipboard.writeText(JSON.stringify(this._config)); } } abstract class BuilderTab { protected _builder: HomebrewBuilder; protected _config: CharacterConfig; protected _content!: Array; constructor(builder: HomebrewBuilder, config: CharacterConfig) { this._builder = builder; this._config = config; } get dom() { return this._content; } }; class PeopleEditor extends BuilderTab { private _options: HTMLDivElement[]; private _activeOption?: HTMLDivElement; constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); this._options = config.peoples.map( (people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => { "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false)); this._activeOption = this._options[i]!; "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true)); } } }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]), ); this._content = [ div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options) ]; } } class TrainingEditor extends BuilderTab { private _options: Record; private _tab: number = 0; private _statIndicator: HTMLSpanElement; private _statContainer: HTMLDivElement; constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); const statRenderBlock = (stat: MainStat) => { return Object.entries(config.training[stat]).map( (level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]), div("flex flex-row gap-4 justify-center", level[1].map((option, j) => dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => { this._builder.edit(config.features[option]!); }}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ]))) ]) } this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record); this._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' }); this._statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(this._options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e])))); this._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: () => this.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' })), this._statIndicator, ]), ]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])]; this.switchTab(0); } switchTab(tab: number) { this._tab = tab; this._statIndicator.setAttribute('data-text', mainStatTexts[MAIN_STATS[tab] as MainStat]); this._statIndicator.style.left = `${tab * 1.5}em`; this._statContainer.style.left = `-${tab * 100}%`; } } class AbilityEditor extends BuilderTab { private _options: HTMLDivElement[]; private _tooltips: Text[] = []; private _maxs: HTMLElement[] = []; constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); const numberInput = (value?: number, update?: (value: number) => number | undefined) => { const input = dom("input", { class: `w-14 mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, listeners: { input: (e: Event) => { input.value = (update && update(parseInt(input.value))?.toString()) ?? input.value; }, keydown: (e: KeyboardEvent) => { let value = isNaN(parseInt(input.value)) ? '0' : input.value; switch(e.key) { case "ArrowUp": value = clamp(parseInt(value) + 1, 0, 99).toString(); break; case "ArrowDown": value = clamp(parseInt(value) - 1, 0, 99).toString(); break; default: break; } if(input.value !== value) { input.value = (update && update(parseInt(value))?.toString()) ?? value; } } }}); input.value = value?.toString() ?? "0"; return input; }; function pushAndReturn(arr: Array, value: T): T { arr.push(value); return value; } /* this._options = ABILITIES.map((e, i) => div('flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative', [ div('flex justify-between', [ numberpicker({ defaultValue: this._builder.character.abilities[e], input: (value) => { const values = this._builder.values; const max = (values[`abilities/${e}/max`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0); this._builder.character.abilities[e] = clamp(value, 0, max); Object.assign((this._options[i]?.lastElementChild as HTMLSpanElement | undefined)?.style ?? {}, { width: `${(max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100}%` }); this._tooltips[i]!.textContent = `${mainStatTexts[config.abilities[e].max[0]]} (${values[`modifier/${config.abilities[e].max[0]}`] ?? 0}) + ${mainStatTexts[config.abilities[e].max[1]]} (${values[`modifier/${config.abilities[e].max[1]}`] ?? 0}) + ${values[`abilities/${e}/max`] ?? 0}`; this._maxs[i]!.textContent = `/ ${max ?? 0}`; const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0); this._pointsInput.value = ((values.ability ?? 0) - abilities).toString(); return this._builder.character.abilities[e]; }}), popper(pushAndReturn(this._maxs, dom('span', { class: 'text-lg text-end cursor-pointer', text: '' })), { arrow: true, offset: 6, placement: 'bottom-end', class: 'max-w-96 fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50', content: [ pushAndReturn(this._tooltips, text('')) ] })]), dom('span', { class: "text-xl text-center font-bold", text: config.abilities[e].name }), dom('span', { class: "absolute -bottom-px -left-px h-[3px] bg-accent-blue" }), ])); */ this._content = [ div('flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48', /* this._options */)]; } } class AspectEditor extends BuilderTab { private _filter: boolean = true; private _options: HTMLDivElement[]; constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); /* this._options = config.aspects.map((e, i) => dom('div', { attributes: { "data-aspect": i.toString() }, listeners: { click: () => { this._builder.character.aspect = i; this._options.forEach(_e => _e.setAttribute('data-state', 'inactive')); this._options[i]?.setAttribute('data-state', 'active'); }}, class: 'group flex flex-col w-[360px] border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 cursor-pointer' }, [ div('bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2 group-data-[state=active]:bg-accent-blue group-data-[state=active]:bg-opacity-10', [ div('flex flex-row gap-8 ps-4 items-center', [ div("flex flex-1 flex-col gap-2 justify-center", [ div('text-lg font-bold', [ text(e.name) ]), dom('span', { class: 'border-b w-full border-light-50 dark:border-dark-50 group-data-[state=active]:border-b-[4px] group-data-[state=active]:border-accent-blue' }) ]), div('rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10') ]) ]), div('flex justify-stretch items-stretch py-2 px-4 gap-4', [ div('flex flex-col flex-1 items-stretch gap-4', [ div('flex flex-1 justify-between', [ text('Difficulté'), div('text-sm font-bold', [ text(e.difficulty.toString()) ]) ]), div('flex flex-1 justify-between', [ text('Bonus'), div('text-sm font-bold', [ text(e.stat === 'special' ? 'Special' : mainStatTexts[e.stat]) ]) ]) ]), div('w-px h-full bg-light-50 dark:bg-dark-50'), div('flex flex-col items-center justify-between py-2', [ div('text-sm italic', [ text(alignmentToString(e.alignment)) ]), div(['text-sm font-bold', { "text-light-purple dark:text-dark-purple italic": e.magic, "text-light-orange dark:text-dark-orange": !e.magic }], [ text(e.magic ? 'Magie autorisée' : 'Magie interdite') ]), ]), ]) ])); */ this._content = [ div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', /* this._options */)]; } } export class FeatureEditor { private _container: HTMLDivElement; private _success?: Function; private _failure?: Function; private _feature?: Feature; private _idInput: HTMLInputElement; private _table: HTMLDivElement; constructor() { this._idInput = dom("input", { attributes: { '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` }); this._table = div('grid grid-cols-2 gap-4 px-2'); this._container = 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 }), () => { this._success!(this._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" }), this._idInput ]), tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => { this._failure!(this._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 = this._feature?.effect.map(e => textFromEffect(e)).join('\n') ?? this._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 }), () => { this._table.appendChild(this._edit({ id: getID(ID_SIZE) })); }, 'p-1'), 'Ajouter', 'left'), ]), this._table, ]) ]); } edit(feature: Feature): Promise { return new Promise((success, failure) => { this._success = success; this._failure = failure; this._feature = JSON.parse(JSON.stringify(feature)) as Feature; this._table.replaceChildren(...this._feature.effect.map(this._renderEffect.bind(this))); this._idInput.value = this._feature.id; MarkdownEditor.singleton.onChange = (e) => this._feature!.description = e; MarkdownEditor.singleton.content = this._feature.description; }); } private _renderEffect(effect: FeatureItem): HTMLDivElement { 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', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]), div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => { this._table.replaceChild(this._edit(effect), content); }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifier", "bottom"), tooltip(button(icon('radix-icons:trash'), () => { this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id); content.remove(); }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Supprimer", "bottom") ]) ]) ]); return content; } private _edit(effect: FeatureItem): HTMLDivElement { const match = (effect: FeatureItem): Partial | undefined => { switch(effect.category) { case 'value': return flattenFeatureChoices.find(e => e.category === 'value' && e.property === effect.property); case 'choice': return flattenFeatureChoices.find(e => e.category === 'choice'); case 'list': return flattenFeatureChoices.find(e => e.category === 'list' && e.list === effect.list); } }; const approve = () => { const idx = this._feature!.effect.findIndex(e => e.id === buffer.id); if(idx === -1) this._feature!.effect.push(buffer); else this._feature!.effect[idx] = buffer; this._table.replaceChild(this._renderEffect(buffer), content); }, reject = () => { const idx = this._feature!.effect.findIndex(e => e.id === buffer.id); if(idx === -1) content.remove(); else this._table.replaceChild(this._renderEffect(effect), content); } let buffer = JSON.parse(JSON.stringify(effect)) as FeatureItem; const redraw = () => { let top: NodeChildren = [], bottom: NodeChildren = []; switch(buffer.category) { case 'value': const summaryText = text(textFromEffect(buffer)); top = [ select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property) || ['defense/'].some(e => (buffer as Extract).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }), typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract).value = value; summaryText.textContent = textFromEffect(buffer); } }), button(icon('radix-icons:update'), () => { (buffer as Extract).value = (typeof (buffer as Extract).value === 'number' ? '' as any as false : 0); const element = redraw(); this._table.replaceChild(element, content); content = element; 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'), ]; bottom = [div('px-2 py-1', [summaryText])]; break; case 'list': if(buffer.action === 'add') { if(buffer.list === 'spells') { bottom = [ combobox(config.spells.map(e => ({ text: e.name, value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as Extract).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' } }) ]; } else { const editor = new MarkdownEditor(); editor.content = buffer.item; editor.onChange = (item) => (buffer as Extract).item = item; bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1', [ editor.dom ]) ]; } } top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => (buffer as Extract).action = value as 'add' | 'remove', class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ]; break; case 'choice': const add = () => { const option: Extract["options"][number] = { id: getID(ID_SIZE), category: 'value', text: '', operation: 'add', property: '', value: 0 }; (buffer as Extract).options.push(option); render(option); }; const render = (option: FeatureEffect) => { } const list = div('flex flex-row'); top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }) ]; break; default: break; } return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [ div('flex flex-row flex-1', [ combobox(featureChoices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => { buffer = { id: buffer.id, ...e } as FeatureItem; const element = redraw(); this._table.replaceChild(element, content); content = element; } }), ...top, ]), div('flex', [ tooltip(button(icon('radix-icons:check'), approve, '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'), reject, '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") ]) ]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-auto items-center', bottom) ]); } let content = redraw(); return content; } get container() { return this._container; } } const featureChoices: Option>[] = [ { text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 0 }, }, { text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 0 }, }, { text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 0 }, }, { text: 'Nombre d\'œuvres maitrisés', value: { category: 'value', property: 'artslots', operation: 'add', value: 0 }, }, { text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 0 }, }, { text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 0 }, }, { text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 0 }, }, { text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 0 }, }, { text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 0 }, }, { text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, }, { text: 'Défense', value: [ { text: 'Défense max', value: { category: 'value', property: 'defense/hardcap', operation: 'add', value: 0 } }, { text: 'Défense fixe', value: { category: 'value', property: 'defense/static', operation: 'add', value: 0 } }, { text: 'Parade active', value: { category: 'value', property: 'defense/activeparry', operation: 'add', value: 0 } }, { text: 'Parade passive', value: { category: 'value', property: 'defense/passiveparry', operation: 'add', value: 0 } }, { text: 'Esquive active', value: { category: 'value', property: 'defense/activedodge', operation: 'add', value: 0 } }, { text: 'Esquive passive', value: { category: 'value', property: 'defense/passivedodge', operation: 'add', value: 0 } } ] }, { text: 'Maitrise', value: [ { text: 'Maitrise des armes (for.)', value: { category: 'value', property: 'mastery/strength', operation: 'add', value: 0 } }, { text: 'Maitrise des armes (dex.)', value: { category: 'value', property: 'mastery/dexterity', operation: 'add', value: 0 } }, { text: 'Maitrise des boucliers', value: { category: 'value', property: 'mastery/shield', operation: 'add', value: 0 } }, { text: 'Maitrise des armure', value: { category: 'value', property: 'mastery/armor', operation: 'add', value: 0 } }, { text: 'Attaque multiple', value: { category: 'value', property: 'mastery/multiattack', operation: 'add', value: 0 } }, { text: 'Arbre de magie (Puissance)', value: { category: 'value', property: 'mastery/magicpower', operation: 'add', value: 0 } }, { text: 'Arbre de magie (Rapidité)', value: { category: 'value', property: 'mastery/magicspeed', operation: 'add', value: 0 } }, { text: 'Arbre de magie (Elements)', value: { category: 'value', property: 'mastery/magicelement', operation: 'add', value: 0 } }, { text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', operation: 'add', value: 0 } } ] }, { text: 'Compétence', value: Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 0 } })) }, { text: 'Modifier', value: [ { text: 'Modifier de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 0 } }, { text: 'Modifier de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 0 } }, { text: 'Modifier de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 0 } }, { text: 'Modifier d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 0 } }, { text: 'Modifier de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 0 } }, { text: 'Modifier de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 0 } }, { text: 'Modifier de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 0 } } ] }, { text: '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: FeatureItem) { if(effect.category === 'value') { 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\'equipement 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).`; default: break; } const splited = effect.property.split('/'); switch(splited[0]) { case 'spellranks': return ''; 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 'mastery': switch(splited[1]) { case 'strength': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise for = interdit).' }); case 'dexterity': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise dex = interdit).' }); case 'shield': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise boucliers = interdit).' }); case 'armor': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise armure = interdit).' }); case 'multiattack': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Attaque multiple ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Attaque multiple fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Attaque multiple = interdit).' }); case 'magicpower': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise puissance = interdit).' }); case 'magicspeed': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise rapidité = interdit).' }); case 'magicelement': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise elements = interdit).' }); case 'magicinstinct': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise instinct = interdit).' }); default: return 'Maitrise inconnue.'; } /* case 'resistance': return splited[1] ? config.resistances[splited[1] as string].name : 'résistance inconnue'; */ case 'abilities': return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${`${config.abilities[splited[1] as Ability].name}.`}` }); case 'modifier': 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': case 'reaction': case 'freeaction': case 'passive': return effect.action === 'add' ? effect.item : 'Suppression d\'effet.'; case 'spells': return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".`; case 'sickness': return effect.action === 'add' ? `Vous subisez en permanence la maladie "${config.lists.sickness.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".` : `Vous ne subisez plus la maladie "${config.lists.sickness.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".`; } } else if(effect.category === 'choice') { return `${effect.text} (${effect.options.length} options)`; } else { 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) 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' : '') ?? ''}`; }