From 893247e1ebcb104c859a1a25ba8925be3edfd324 Mon Sep 17 00:00:00 2001 From: Peaceultime Date: Tue, 26 Aug 2025 00:17:08 +0200 Subject: [PATCH] Aspect and Spell editor, multiselect component. --- db.sqlite | Bin 761856 -> 761856 bytes db.sqlite-shm | Bin 32768 -> 32768 bytes db.sqlite-wal | Bin 41232 -> 0 bytes shared/character-config.json | 2264 ++++++++++++++++++++++++++-------- shared/character.util.ts | 12 +- shared/components.util.ts | 95 +- shared/feature.util.ts | 148 ++- types/character.d.ts | 1 + 8 files changed, 1971 insertions(+), 549 deletions(-) diff --git a/db.sqlite b/db.sqlite index 45c1e17d038c83dfab6dbe04b4b13bc023524f5b..619ecd40fcb5ed4b132cd84e59772c0c87b14e01 100644 GIT binary patch delta 228 zcmZoTpx1CfZv#^R+XM!Fo=*&uxdLQa_wl{q`Lx+k;RMg-Pcict+4wIp=y9@4?#WXz z%4m7qpd-N&%E@AoXl!O~Y+|ZwVw?mdQj?Q)ElpDlb(1WOEDcOk(hQ6(O)}P8emD6| zoDNfy$YiPb7aZ*j{Jgvj{4A3>5)^>uKjzuYn_I=m!oa{#JGncrQcDwPFe5PrOKjH5 uzaYSL`CU^(TLWWT15;ZAb6W#TTLWub16x}Ids_oXTLWiX1J|+!ZUq3hEk=O= delta 198 zcmZoTpx1CfZv#^R$36yro=*&X6DD&6$TGj-`Lx+k;RMg-Pcict*>pHrLOEF`_vEQ? zWwbnQ;J?J62jp(cQ(?(yKK^C$n>Zb&2)4;m@h{jmGVt^AGEC-5P+;O~2b%DhXESeZ z6(fr#(16L^d6io13=9mljFzTGX2z*$rn&}+Mv1y6scDJ27Kv#Vx=E(VDJDrqCZ-mN x7Mu0*F9-n5ZE9$1U~Fq(YHMI_YhY<>U~Ow)YinR{Yv5>W;B0H)TGqg=000(^KL!8* diff --git a/db.sqlite-shm b/db.sqlite-shm index d4b1a266c8af5f2c2a34774a255bec33c6bf4446..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10 100644 GIT binary patch delta 80 zcmZo@U}|V!;+1%$%K!t66E{kWTChv7nNGgVi7p`mlYpuI4+Il)WHuf!3EX(VMGpX^ Cc@#we delta 211 zcmZo@U}|V!s+V}A%K!qLK+MR%AixErcLA|p_i?Y(10wU|&9iD}Ps&Zwl~A)VYB9b+ zs(PSNU;r}rKN5fnPps!uWdpL=ffz({;G#D+x(9MHGB7i+0@ZSEZ2Zc^$h5KXFB3Zp NP#*^a*TzOp4FJTKF5CbB diff --git a/db.sqlite-wal b/db.sqlite-wal index 8bcaf9cd284aaed50f1cd46ca8d202cc2ca3a109..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 41232 zcmeI&Ux*b|90%}wWnEj{neorMN-y5GLD zq~#VYCTUbJX%LaEW%giTK|%E9L-msVdz1b_#X`3EVo>y(S(5Z&oyT_fdoRo0z5AV+ z-}%hkf&1g9o*JC!vG3RSdMkU)<6-s4{^Kw1w%hLAxM$bYiTCKO!T-KLyzlbwpT0Xf z6@)MJ!r_@-XhRvD2nU02gHyqi!It3W;hAV}v?;nVoNk^J!2<#ifB*y_009U<00Izz zz&sO(*9>eQ8mLm4mAq0cGgdGuvXq&`dFG{Resd)gRZneR60dI3PKvZLRk|*$7O!g3R#GZ+70YFjFlj2o49|0>v`@6il~rEVZ)}>1SGN03 zQk@7cSymOa@7yb9U7oR2mO62~;5k?It7o2wqb6;r+`=fyC}Nnjv|W)UIlW4)G^J9G z>-zZCU*fPym(t0U3(ayRQYNKJnRZGrD>Qe$sH{|Z{mRU~IB3$o^gj2-u}mq=WI-hx zs*NhqC3zHOq4WCaJ2%BEnsh-@Qc$&CSgM=TCPs;76H}&w7v5x9ePsO6czIii5;`kf z#*EjJs?9yKL;&=!VlPFH^;v zVmO!efr$;2UPeD3{@X7&b#Ze2f!m+ovpk&ag+b7ZPDfvdA~+kEXd>DXjRhCNhl0a_ z3WtIqoNS)+?BkDeApijgKmY;|fB*y_009WhC2%W!|LS+?7_4cNUAhIU+hmtc!K&GP zcIXnUY?EC&1W}vp(j5rfWS7oB&?dWd1y;-+y+cP}Ih~c0{Vv^rAxiG=cj*M&K*{_1 z$|p42^Hvy}BOUc~96M?hnu# z<9FW`)~64iCxENw1*Ug>^#1u@4=u&Kz{0;YutEqx00Izz00bZa0SGK!0n7_vUZBIg z!1Usr7bwquIrH)nlVV { - this._builder.character.visibility = this._builder.character.visibility === "private" ? "public" : "private"; - this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked"); - } - }}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]); + this._visibilityInput = toggle({ defaultValue: this._builder.character.visibility === "private", change: (value) => this._builder.character.visibility = value ? "private" : "public" }); 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: () => { diff --git a/shared/components.util.ts b/shared/components.util.ts index 149866f..0d3c011 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -50,7 +50,7 @@ export function select>(options: Array<{ text: string return dom('div', { listeners: { click: () => { textValue.textContent = e.text; settings?.change && settings?.change(e.value); - close && close(); + context && context.close && context.close(); }, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]); }); const select = dom('div', { listeners: { click: () => { @@ -96,6 +96,78 @@ export function select>(options: Array<{ text: string }) return select; } +export function multiselect>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement +{ + let context: { close: Function }; + let focused: number | undefined; + let selection: T[] = settings?.defaultValue ?? []; + + options = options.filter(e => !!e); + + const focus = (i?: number) => { + focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false); + i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' }); + focused = i; + } + + let disabled = settings?.disabled ?? false; + const textValue = text(selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : ''); + const optionElements = options.map((e, i) => { + if(e === undefined) + return; + + const element = dom('div', { listeners: { click: () => { + selection = selection.includes(e.value) ? selection.filter(f => f !== e.value) : [...selection, e.value]; + textValue.textContent = selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : ''; + element.toggleAttribute('data-selected', selection.includes(e.value)); + settings?.change && settings?.change(selection); + context && context.close && context.close(); + }, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': selection.includes(e.value) } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block', noobserver: true }) ]); + return element; + }); + const select = dom('div', { listeners: { click: () => { + if(disabled) + return; + + const handleKeys = (e: KeyboardEvent) => { + switch(e.key.toLocaleLowerCase()) + { + case 'arrowdown': + focus(clamp((focused ?? -1) + 1, 0, options.length - 1)); + return; + case 'arrowup': + focus(clamp((focused ?? 1) - 1, 0, options.length - 1)); + return; + case 'pageup': + focus(0); + return; + case 'pagedown': + focus(optionElements.length - 1); + return; + case 'enter': + focused && optionElements[focused]?.click(); + return; + case 'escape': + context?.close(); + return; + default: return; + } + } + window.addEventListener('keydown', handleKeys); + + const box = select.getBoundingClientRect(); + context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => window.removeEventListener('keydown', handleKeys) }); + } }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]); + + Object.defineProperty(select, 'disabled', { + get: () => disabled, + set: (v) => { + disabled = !!v; + select.toggleAttribute('data-disabled', disabled); + }, + }) + return select; +} export function combobox>(options: Option[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean, fill?: 'contain' | 'cover' }): HTMLElement { let context: { container: HTMLElement, content: NodeChildren, close: () => void }; @@ -294,7 +366,7 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value focus: () => settings?.focus && settings.focus(), blur: () => settings?.blur && settings.blur(), }}); - if(settings?.defaultValue) field.value = storedValue.toString(10); + if(settings?.defaultValue !== undefined) field.value = storedValue.toString(10); return field; } @@ -309,8 +381,23 @@ export function foldable(content: NodeChildren, title: NodeChildren, settings?: return fold; } type TableRow = Record HTMLElement) | HTMLElement | string>; -export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class } }) +export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } }) { const render = (item: (() => HTMLElement) | HTMLElement | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item; - return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: '' }, [ render(e[f]!) ]) : undefined)))) ]); + return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: ['', properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]); +} +export function toggle(settings?: { defaultValue?: boolean, change?: (value: boolean) => void, disabled?: boolean, class?: { container?: Class } }) +{ + let state = settings?.defaultValue ?? false; + const element = dom("div", { class: [`group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none + data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40 + data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked" }, listeners: { + click: (e: Event) => { + state = !state; + element.setAttribute('data-state', state ? "checked" : "unchecked"); + settings?.change && settings.change(state); + } + } + }, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]); + return element; } \ No newline at end of file diff --git a/shared/feature.util.ts b/shared/feature.util.ts index fe3296f..2098ee7 100644 --- a/shared/feature.util.ts +++ b/shared/feature.util.ts @@ -1,12 +1,12 @@ -import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance } from "~/types/character"; +import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance, SpellConfig } from "~/types/character"; import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util"; import { MarkdownEditor } from "#shared/editor.util"; import { fakeA } from "#shared/proses"; -import { button, combobox, foldable, input, numberpicker, select, table, type Option } from "#shared/components.util"; +import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util"; import { fullblocker, tooltip } from "#shared/floating.util"; -import { elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, spellTypeTexts } from "#shared/character.util"; +import { ALIGNMENTS, alignmentToString, elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util"; import characterConfig from "#shared/character-config.json"; -import { clamp, getID, ID_SIZE } from "#shared/general.util"; +import { getID, ID_SIZE } from "#shared/general.util"; import renderMarkdown, { renderText } from "#shared/markdown.util"; import { Tree } from "#shared/tree"; import markdownUtil from "#shared/markdown.util"; @@ -42,8 +42,8 @@ export class HomebrewBuilder new TrainingEditor(this, this._config), new AbilityEditor(this, this._config), new AspectEditor(this, this._config), - /* new SpellEditor(this), - new ListEditor(this), */ + new SpellEditor(this, this._config), + /* 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', [ @@ -173,53 +173,115 @@ class AbilityEditor extends BuilderTab constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); - - Object.entries(config.abilities).map(e => div('flex flex-col gap-4 border border-light-25 dark:border-dark-25', [ ])) - this._content = [ table(Object.entries(config.abilities).map(e => ({ - max1: div('', [ text(mainStatTexts[e[1].max[0]]) ]), - max2: div('', [ text(mainStatTexts[e[1].max[1]]) ]), - name: div('', [ text(e[1].name) ]), - description: div('', [ text(e[1].description) ]), - id: div('', [ text(e[0]) ]), - })), { id: 'ID', name: 'Nom', description: 'Description', max1: 'Stat 1', max2: 'Stat 2' }) ]; + this._content = [ div('flex px-24 py-4', [table(Object.entries(config.abilities).map(e => ({ + max1: select(MAIN_STATS.map(e => ({ text: mainStatTexts[e], value: e })), { change: (value) => e[1].max[0] = value, defaultValue: e[1].max[0], class: { container: 'w-full !m-0' } }), + max2: select(MAIN_STATS.map(e => ({ text: mainStatTexts[e], value: e })), { change: (value) => e[1].max[1] = value, defaultValue: e[1].max[1], class: { container: 'w-full !m-0' } }), + name: input('text', { input: (value) => e[1].name = value, placeholder: 'Nom', defaultValue: e[1].name, class: 'w-full !m-0' }), + description: input('text', { input: (value) => e[1].description = value, placeholder: 'Description', defaultValue: e[1].description, class: 'w-full !m-0' }), + id: div('w-full !m-0', [ text(e[0]) ]), + })), { id: 'ID', name: 'Nom', description: 'Description', max1: 'Stat 1', max2: 'Stat 2' }, { class: { table: 'flex-1' } })] ) ]; } } class AspectEditor extends BuilderTab { - private _filter: boolean = true; - - private _options: HTMLDivElement[]; - constructor(builder: HomebrewBuilder, config: CharacterConfig) { super(builder, config); + + 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: alignmentToString(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 gap-2', [ numberpicker({ defaultValue: aspect.physic.min, input: (value) => aspect.physic.min = value }), numberpicker({ defaultValue: aspect.physic.max, input: (value) => aspect.physic.max = value }) ]), + mental: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.mental.min, input: (value) => aspect.mental.min = value }), numberpicker({ defaultValue: aspect.mental.max, input: (value) => aspect.mental.max = value }) ]), + personality: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.personality.min, input: (value) => aspect.personality.min = value }), numberpicker({ defaultValue: aspect.personality.max, input: (value) => aspect.personality.max = value }) ]), + 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 = () => { + config.aspects.push({ + name: '', + description: '', + stat: 'strength', + alignment: { kindness: 'good', loyalty: 'loyal' }, + magic: false, + difficulty: 6, + physic: { min: 0, max: 30 }, + mental: { min: 0, max: 20 }, + personality: { min: 0, max: 20 }, + options: [] + }); - /* 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') ]), - ]), - ]) - ])); */ + const element = redraw(); + content.parentElement?.replaceChild(element, content); + content = element; + }; + const remove = (aspect: AspectConfig) => { + config.aspects = config.aspects.filter(e => e !== aspect); - this._content = [ div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', /* this._options */)]; + const element = redraw(); + content.parentElement?.replaceChild(element, content); + content = element; + } + const redraw = () => table(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(); + this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ]; + } +} +class SpellEditor extends BuilderTab +{ + constructor(builder: HomebrewBuilder, config: CharacterConfig) + { + super(builder, config); + + const render = (spell: SpellConfig) => { + return { + id: spell.id, + name: input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-full' }), + rank: 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 w-full' } }), + type: select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 w-full' } }), + cost: numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), + speed: 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 w-full' } }), + elements: multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 w-full' } }), + effect: input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }), + 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 w-full' } }), + concentration: toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0' } }), + action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ]) + }; + } + const add = () => { + config.spells.push({ + id: getID(ID_SIZE), + name: '', + rank: 1, + type: 'precision', + cost: 1, + speed: 'action', + elements: [], + effect: '', + concentration: false, + tags: [], + }); + + const element = redraw(); + content.parentElement?.replaceChild(element, content); + content = element; + }; + const remove = (spell: SpellConfig) => { + config.spells = config.spells.filter(e => e !== spell); + + const element = redraw(); + content.parentElement?.replaceChild(element, content); + content = element; + } + const redraw = () => table(config.spells.map(render), { id: 'ID', name: 'Nom', rank: 'Rang', type: 'Type', cost: 'Coût', speed: 'Incantation', elements: 'Elements', effect: 'Effet', tags: 'Tag', concentration: 'Concentration', action: 'Actions' }, { class: { table: 'flex-1' } }); + let content = redraw(); + this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ]; } } diff --git a/types/character.d.ts b/types/character.d.ts index d69a09d..f028aa5 100644 --- a/types/character.d.ts +++ b/types/character.d.ts @@ -63,6 +63,7 @@ export type SpellConfig = { speed: "action" | "reaction" | number; elements: Array; effect: string; + concentration: boolean; tags?: string[]; }; export type AbilityConfig = {