From b1ac379f1a67db58a311f1d7f2e0f7fe46af080a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Mon, 29 Sep 2025 17:53:39 +0200 Subject: [PATCH 1/4] Work in progress: CharacterSheet implementation and FeatureChoice rework --- shared/feature.util.ts | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/shared/feature.util.ts b/shared/feature.util.ts index 222d6db..a6f7925 100644 --- a/shared/feature.util.ts +++ b/shared/feature.util.ts @@ -1,4 +1,4 @@ -import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character"; +import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } 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"; @@ -458,14 +458,14 @@ export class FeatureEditor switch(buffer.category) { case 'value': - const valueVariable = () => 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); } }); + const valueVariable = () => typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as FeatureValue).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 FeatureValue).value = value; summaryText.textContent = textFromEffect(buffer); } }); const summaryText = text(textFromEffect(buffer)); let valueSelection = valueVariable(); top = [ - select([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].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]' } }), + select([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].includes(buffer.property ?? '') || ['defense/'].some(e => (buffer as FeatureValue).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as FeatureValue).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }), valueSelection, tooltip(button(icon('radix-icons:update'), () => { - (buffer as Extract).value = (typeof (buffer as Extract).value === 'number' ? '' as any as false : 0); + (buffer as FeatureValue).value = (typeof (buffer as FeatureValue).value === 'number' ? '' as any as false : 0); const newValueSelection = valueVariable(); valueSelection.replaceWith(newValueSelection); valueSelection = newValueSelection; @@ -479,13 +479,13 @@ export class FeatureEditor { if(buffer.list === 'spells') { - bottom = [ combobox(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(renderText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as Extract).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ]; + bottom = [ combobox(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(renderText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ]; } else { const editor = new MarkdownEditor(); editor.content = getText(buffer.item); - editor.onChange = (item) => (buffer as Extract).item = item; + editor.onChange = (item) => (buffer as FeatureList).item = item; bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1 flex items-center', [ editor.dom ]) ]; } @@ -495,7 +495,7 @@ export class FeatureEditor bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map(e => ({ text: buffer.list !== 'spells' ? renderText(getText((e as Extract).item)) : config.spells.find(f => f.id === (e as Extract).item)?.name ?? '', value: (e as Extract).item })), { 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' }) ]; } top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => { - (buffer as Extract).action = value as 'add' | 'remove'; + (buffer as FeatureList).action = value as 'add' | 'remove'; const element = redraw(); content.replaceWith(element); content = element; @@ -601,13 +601,13 @@ const featureChoices: Option>[] = [ { text: 'Modifieur de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } }, { text: 'Modifieur de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } }, { text: 'Modifieur au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [ - { text: 'Modifieur de force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 }, - { text: 'Modifieur de dextérité', category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 }, - { text: 'Modifieur de constitution', category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 }, - { text: 'Modifieur d\'intelligence', category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 }, - { text: 'Modifieur de curiosité', category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 }, - { text: 'Modifieur de charisme', category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 }, - { text: 'Modifieur de psyché', egory: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } + { text: 'Modifieur de force', effects: [ { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } ] }, + { text: 'Modifieur de dextérité', effects: [ { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } ] }, + { text: 'Modifieur de constitution', effects: [ { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } ] }, + { text: 'Modifieur d\'intelligence', effects: [ { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } ] }, + { text: 'Modifieur de curiosité', effects: [ { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } ] }, + { text: 'Modifieur de charisme', effects: [ { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } ] }, + { text: 'Modifieur de psyché', effects: [ { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } ] } ]} as Partial} ] }, { text: 'Jet de résistance', value: [ @@ -619,14 +619,14 @@ const featureChoices: Option>[] = [ { text: 'Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } }, { text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } }, { text: 'Résistance au choix', value: { category: 'choice', text: '+1 au jet de résistance de ', options: [ - { text: 'Force', category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 }, - { text: 'Dextérité', category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 }, - { text: 'Constitution', category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 }, - { text: 'Intelligence', category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 }, - { text: 'Curiosité', category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 }, - { text: 'Charisme', category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 }, - { text: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } - ]} as Partial} + { text: 'Force', effects: [{ category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 }] }, + { text: 'Dextérité', effects: [{ category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 }] }, + { text: 'Constitution', effects: [{ category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 }] }, + { text: 'Intelligence', effects: [{ category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 }] }, + { text: 'Curiosité', effects: [{ category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 }] }, + { text: 'Charisme', effects: [{ category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 }] }, + { text: 'Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }] } + ]} as Partial} ] }, { text: 'Bonus', value: RESISTANCES.map(e => ({ text: resistanceTexts[e as Resistance], value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) }, { text: 'Rang', value: [ @@ -643,7 +643,7 @@ const featureChoices: Option>[] = [ { 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 +function textFromEffect(effect: Partial): string { if(effect.category === 'value') { From 1642cd513fe6f022a9ba9a75e36a3ff2cbbd568f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Mon, 29 Sep 2025 17:53:41 +0200 Subject: [PATCH 2/4] Work in progress: CharacterSheet implementation and FeatureChoice rework --- components/prose/PreviewA.vue | 4 +- db.sqlite | Bin 761856 -> 761856 bytes db.sqlite-shm | Bin 32768 -> 32768 bytes db.sqlite-wal | Bin 32992 -> 8272 bytes pages/character/[id]/index.client.vue | 125 ++-- shared/canvas.util.ts | 4 +- shared/character-config.json | 801 ++++++++++---------------- shared/character.util.ts | 389 ++++++++++++- shared/components.util.ts | 40 ++ shared/dom.util.ts | 12 +- shared/feature.util.ts | 87 +-- shared/proses.ts | 18 +- types/character.d.ts | 73 ++- 13 files changed, 889 insertions(+), 664 deletions(-) diff --git a/components/prose/PreviewA.vue b/components/prose/PreviewA.vue index d1b0db5..5b6ed4d 100644 --- a/components/prose/PreviewA.vue +++ b/components/prose/PreviewA.vue @@ -4,7 +4,7 @@ \ No newline at end of file diff --git a/db.sqlite b/db.sqlite index 61ed8486f101ef7f1642ef0fc5177d14d92be4bf..827b942840d0381ee4ff23b27394b0c04119c9aa 100644 GIT binary patch delta 475 zcma)%KS)AB9LMkFy=R)H6NT;=yYlyrrWCKhX|76y_b_EuCzds8!UWBl=6ni>IgyATuUK7=-h0Ag}P4G4Ug5 zV3L&u#Ih6vWFJ!5XZIcR6G9p=YMJyKmMqgi*vIfiMZl3odzwalsEBmwV4F<@=(>l_ zOxat-++;BlvlCO1q@B!1X5w}}lF!+(e9BG~wx$!M`?I!}Zx9!!gj3$((cbufIjqKN z*z&~(eh1g@XlK!Vm1gffLs8W<`T~Mi?A1q`^~0=!|#BJNV1JXL6&#Y*s+J z{GsFGyBY*7VGRG^vL*B(#5Jf_#zX(95V)*BjAfw3@B*iunz;&RAL^UcgRnG7B2bfgagM;UY?A!RBd6L*meKVTnT z!V*j=Q+$DocpXj9tH_Bmec4LADSTShWo;Fdqk!*wB-ZF2tJ;cRo_QqFxGVOwWl-kS zR)K~EJk9_K__S47gT9HHgc~&gw7BS7wQ+bjI?$4WHyj}+1ROd-Ui0Or7U8#!kQLFt zchrg`NqQ8xuCZNk^KRC)EGui~EHmpGg6d|jU=(&vO~Wu7S8WnQwuB&*Fu)J^9p&K= zy10!iSchYH4GSm=MW7Dqi2uG74m}lQ#9_YYrRPvzz>Sv<%|-z}u!)T4_+^t2@)uxp zG2X2aUTyyT%PEJ*$+=u|NGRdUgVrYRJ0zeypa0^Z_`gjV4KTXIXpm9BXoyjn(J-SC MMx%_zdNiK;19_ixdH?_b diff --git a/db.sqlite-shm b/db.sqlite-shm index 95530a0c9878c07e09c5323094325064e1bbb687..6449b620c75646a659c8af68d3e08110e97f011a 100644 GIT binary patch delta 156 zcmZo@U}|V!s+V}A%K!pQK+MR%AONCw0dcXEXixLsP)${l=6COAhE_@hNN;QIpKn2` ndZ1BY05bPK5`YR%tQXw4!Y6Rz0SQLNjT?WnGBR!4$gcqah(#}e delta 187 zcmZo@U}|V!s+V}A%K!o#K+MR%AONCw0dcO*8_`QPvWYqeUA=E7d9FH_5FEF0SLS6> z)dP(J1CY7@kpNV9V!fa!8;}i>Mxi%O^bcfbWMF1sWnkYp@hdAk69WrS1;@sToEiXh C-!NGK diff --git a/db.sqlite-wal b/db.sqlite-wal index 54e6c1bbe4a18e0204f55a2d8dbefa5476a4ff66..de0a6367cc2d7290ce7c3f87f6f94b80f6b58e13 100644 GIT binary patch delta 282 zcmaFR$aKLW%DkSfi9z>~1OtNr0|=;!G{1W{Gqmz>mbk^2O-V{NNU=yZPE9m7&bYN&7-$d&vX^>9rHVB} P|2A(95ctc_3{(IBcXC+H delta 1014 zcmccM@Su??%DkSfi9z>~1OtNr0|@9GboIWSg=ZJk%?7#g$AvPf11ry)waP83M zlg$5k`H~p;cs?=kneZ>+W8=NSyNb7l*N<0$=M#S!zX!h*-!!3$(Kff(FCMgiY__N@nMb;N+|Fn*w*D3H+t zVu%14n?MX6AmahQmK)H4dYmkYi3VooiN=Y#i53zCc23RKvt@Wk-34nWwMEZ zfk{SPlL}BZ2eOwY^!|Jn@3KmMbAZ5Kep{fGKuHuo^Hij*y(-i1MlU} V-p`_MbaE4AZ7zZ1nR)9$IRK$f7pDLK diff --git a/pages/character/[id]/index.client.vue b/pages/character/[id]/index.client.vue index 8a712ad..b7397a8 100644 --- a/pages/character/[id]/index.client.vue +++ b/pages/character/[id]/index.client.vue @@ -2,25 +2,16 @@ import characterConfig from '#shared/character-config.json'; import { Icon } from '@iconify/vue/dist/iconify.js'; import PreviewA from '~/components/prose/PreviewA.vue'; -import { clamp } from '#shared/general.util'; +import { clamp, unifySlug } from '#shared/general.util'; import type { CompiledCharacter, SpellConfig } from '~/types/character'; import type { CharacterConfig } from '~/types/character'; -import { abilityTexts, CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '#shared/character.util'; +import { abilityTexts, CharacterCompiler, CharacterSheet, defaultCharacter, elementTexts, spellTypeTexts } from '#shared/character.util'; import { getText } from '#shared/i18n'; -import { fakeA } from '#shared/proses'; +import { preview } from '#shared/proses'; import { div, dom, icon, text } from '#shared/dom.util'; import markdown from '#shared/markdown.util'; import { button, foldable } from '#shared/components.util'; import { fullblocker, tooltip } from '~/shared/floating.util'; - -const config = characterConfig as CharacterConfig; - -const id = useRouter().currentRoute.value.params.id; -const { user } = useUserSession(); - -const { data, status, error } = await useFetch(`/api/character/${id}`); -const compiler = new CharacterCompiler(data.value ?? defaultCharacter); -const character = ref(compiler.compiled); /* text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue @@ -33,76 +24,26 @@ text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yel text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple */ -function openSpellPanel() { - const availableSpells = Object.values(config.spells).filter(spell => { - if (spell.rank === 4) return false; - if (character.value.spellranks[spell.type] < spell.rank) return false; - return true; - }); +const config = characterConfig as CharacterConfig; - const textAmount = text(character.value.variables.spells.length.toString()), textMax = text(character.value.spellslots.toString()); - const container = div("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-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [ - div("flex flex-row justify-between items-center mb-4", [ - dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }), - div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ textAmount, text(' / '), textMax, text(' sorts maitrisés') ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => { - setTimeout(blocker.close, 150); - container.setAttribute('data-state', 'inactive'); - }, "p-1"), "Fermer", "left") ]) - ]), - div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', availableSpells.map(spell => { - let state = character.value.lists.spells?.includes(spell.id) ? 'given' : character.value.variables.spells.includes(spell.id) ? 'choosen' : 'empty'; - const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => { - if(state === 'choosen') - { - compiler.variable('spells', character.value.variables.spells.filter(e => e !== spell.id)); - state = 'empty'; - } - else if(state === 'empty') - { - compiler.variable('spells', [...character.value.variables.spells, spell.id]); - state = 'choosen'; - } - character.value = compiler.compiled; - toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'; - textAmount.textContent = character.value.variables.spells.length.toString(); - }, "px-2 py-1 text-sm font-normal"); - toggleButton.disabled = state === 'given'; - return foldable(() => [ - markdown(spell.effect), - ], [ div("flex flex-row justify-between gap-2", [ - dom("span", { class: "text-lg font-bold", text: spell.name }), - div("flex flex-row items-center gap-6", [ - div("flex flex-row text-sm gap-2", - spell.elements.map(el => - dom("span", { - class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[el].class], - text: elementTexts[el].text - }) - ) - ), - div("flex flex-row text-sm gap-1", [ - ...(spell.rank !== 4 ? [ - dom("span", { text: `Rang ${spell.rank}` }), - text("/"), - dom("span", { text: spellTypeTexts[spell.type] }), - text("/") - ] : []), - dom("span", { text: `${spell.cost} mana` }), - text("/"), - dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` }) - ]), - toggleButton, - ]), - ]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } }); - })) - ]); - const blocker = fullblocker([ container ], { closeWhenOutside: true }); - setTimeout(() => container.setAttribute('data-state', 'active'), 1); -} +const id = useRouter().currentRoute.value.params.id ? unifySlug(useRouter().currentRoute.value.params.id!) : undefined; +const { user } = useUserSession(); +const container = useTemplateRef('container'); + +onMounted(() => { + queueMicrotask(() => { + if(container.value && id) + { + const character = new CharacterSheet(id, user); + container.value.appendChild(character.container); + } + }); +}); \ No newline at end of file diff --git a/shared/canvas.util.ts b/shared/canvas.util.ts index def2532..7712d72 100644 --- a/shared/canvas.util.ts +++ b/shared/canvas.util.ts @@ -4,7 +4,7 @@ import { dom, icon, svg, text } from "#shared/dom.util"; import render from "#shared/markdown.util"; import { popper, tooltip } from "#shared/floating.util"; import { History } from "#shared/history.util"; -import { fakeA } from "#shared/proses"; +import { preview } from "#shared/proses"; import { SpatialGrid } from "#shared/physics.util"; import type { CanvasPreferences } from "~/types/general"; @@ -189,7 +189,7 @@ export class NodeEditable extends Node this.nodeDom = dom('div', { class: ['absolute group', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [ dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full group-hover:outline-4', style.border, style.outline] }, [ - dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'text' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: fakeA } })]) : undefined]) + dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'text' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: preview } })]) : undefined]) ]) ]); diff --git a/shared/character-config.json b/shared/character-config.json index 8b8c8a8..ccbf1df 100644 --- a/shared/character-config.json +++ b/shared/character-config.json @@ -4877,28 +4877,40 @@ "text": "Vous avez un bonus de +1 aux jets de résistance de ", "options": [ { - "id": "sx1vca2kzustsjatvslbjl68guv45m0b", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 + "text": "Force", + "effects": [ + { + "id": "sx1vca2kzustsjatvslbjl68guv45m0b", + "category": "value", + "operation": "add", + "property": "bonus/defense/strength", + "value": 1 + } + ] }, { - "id": "41mflh7px0otbj169q8mr5btc8qie18g", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 + "text": "Dextérité", + "effects": [ + { + "id": "41mflh7px0otbj169q8mr5btc8qie18g", + "category": "value", + "operation": "add", + "property": "bonus/defense/dexterity", + "value": 1 + } + ] }, { - "id": "55vp7dpdto073hrqg11aemyxxo9skg0q", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 + "text": "Constitution", + "effects": [ + { + "id": "55vp7dpdto073hrqg11aemyxxo9skg0q", + "category": "value", + "operation": "add", + "property": "bonus/defense/constitution", + "value": 1 + } + ] } ] }, @@ -5074,28 +5086,40 @@ }, "options": [ { - "id": "sx1vca2kzustsjatvslbjl68guv45m0b", - "category": "value", "text": "Force", - "operation": "add", - "property": "bonus/defense/strength", - "value": 1 + "effects": [ + { + "id": "sx1vca2kzustsjatvslbjl68guv45m0b", + "category": "value", + "operation": "add", + "property": "bonus/defense/strength", + "value": 1 + } + ] }, { - "id": "41mflh7px0otbj169q8mr5btc8qie18g", - "category": "value", "text": "Dextérité", - "operation": "add", - "property": "bonus/defense/dexterity", - "value": 1 + "effects": [ + { + "id": "41mflh7px0otbj169q8mr5btc8qie18g", + "category": "value", + "operation": "add", + "property": "bonus/defense/dexterity", + "value": 1 + } + ] }, { - "id": "55vp7dpdto073hrqg11aemyxxo9skg0q", - "category": "value", "text": "Constitution", - "operation": "add", - "property": "bonus/defense/constitution", - "value": 1 + "effects": [ + { + "id": "55vp7dpdto073hrqg11aemyxxo9skg0q", + "category": "value", + "operation": "add", + "property": "bonus/defense/constitution", + "value": 1 + } + ] } ] } @@ -5243,28 +5267,40 @@ "text": "Une fois par [[3. Glossaire#Long repos|long repos]], vous pouvez réussir votre [[3. Résistance aux chocs#Le jet de résistance|jet de résistance]] de cette statistique sans lancer de dés.", "options": [ { - "id": "sx1vca2kzustsjatvslbjl68guv45m0b", - "category": "value", "text": "Force", - "operation": "add", - "property": "bonus/defense/strength", - "value": 1 + "effects": [ + { + "id": "sx1vca2kzustsjatvslbjl68guv45m0b", + "category": "value", + "operation": "add", + "property": "bonus/defense/strength", + "value": 1 + } + ] }, { - "id": "41mflh7px0otbj169q8mr5btc8qie18g", - "category": "value", "text": "Dextérité", - "operation": "add", - "property": "bonus/defense/dexterity", - "value": 1 + "effects": [ + { + "id": "41mflh7px0otbj169q8mr5btc8qie18g", + "category": "value", + "operation": "add", + "property": "bonus/defense/dexterity", + "value": 1 + } + ] }, { - "id": "55vp7dpdto073hrqg11aemyxxo9skg0q", - "category": "value", "text": "Constitution", - "operation": "add", - "property": "bonus/defense/constitution", - "value": 1 + "effects": [ + { + "id": "55vp7dpdto073hrqg11aemyxxo9skg0q", + "category": "value", + "operation": "add", + "property": "bonus/defense/constitution", + "value": 1 + } + ] } ] } @@ -5768,87 +5804,14 @@ ] }, "dxlevxrlacugpj4jvdjs5bxecraoxbnp": { - "description": "Choisissez une [[1. Magie#Les éléments|classe élémentaire]]. Lorsque vous voyez un sort de cet élément être lancé à 12 cases de vous, vous pouvez [[2. Actions en combat#Saisir une opportunité|saisir l'opportunité]] pour dépenser l'intégralité du coût en mana à la place du lanceur. *Vous appliquez le coût en mana du lanceur d'origine.*", + "description": "Choisissez une [[1. Magie#Les éléments|classe élémentaire]]. Lorsque vous voyez un sort de cet élément être lancé à 12 cases de vous, vous pouvez [[2. Actions en combat#Saisir une opportunité|saisir l'opportunité]] pour dépenser l'intégralité du coût en mana à la place du lanceur. *Vous appliquez le coût en mana du lanceur d'origine.* #todo", "id": "dxlevxrlacugpj4jvdjs5bxecraoxbnp", "effect": [ { "id": "ix2y02up7p04hzv1bhyer0cnl5eedjj3", "category": "choice", "text": "Lorsque vous voyez un sort de cet élément être lancé à 12 cases de vous, vous pouvez saisir l'opportunité pour dépenser l'intégralité du coût en mana à la place du lanceur.", - "options": [ - { - "id": "hrz76l4hu874uyz1to2yh2cej71hyk67", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "yp8ito93tx24f8htq4fdougeyjf31y85", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "o8v66orebhiemsnn4rkffs705sqjml0f", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "w7hvncya5m7xbi6igda7r9tlbdkuyg1t", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "ubtwd3sl3y27heps9ev99w8piur2l0zv", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "7kwst41c2eecgop178rfcszidz2t44pf", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "47aqw1fy16dszuircpp1vxco2twq9gah", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "xz3bfma0nh3q7csn83kt2ftij6irpxsg", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "vtowf22lk7gl0rgpjfkss8uh50peaj3o", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - } - ] + "options": [] } ] }, @@ -6993,52 +6956,80 @@ "options": [ { "text": "Force", - "category": "value", - "property": "modifier/strength", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/strength", + "operation": "add", + "value": 1 + } + ] }, { "text": "Dextérité", - "category": "value", - "property": "modifier/dexterity", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/dexterity", + "operation": "add", + "value": 1 + } + ] }, { "text": "Constitution", - "category": "value", - "property": "modifier/constitution", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/constitution", + "operation": "add", + "value": 1 + } + ] }, { "text": "Intelligence", - "category": "value", - "property": "modifier/intelligence", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/intelligence", + "operation": "add", + "value": 1 + } + ] }, { "text": "Curiosité", - "category": "value", - "property": "modifier/curiosity", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/curiosity", + "operation": "add", + "value": 1 + } + ] }, { "text": "Charisme", - "category": "value", - "property": "modifier/charisma", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/charisma", + "operation": "add", + "value": 1 + } + ] }, { "text": "Psyché", - "egory": "value", - "property": "modifier/psyche", - "operation": "add", - "value": 1 + "effects": [ + { + "egory": "value", + "property": "modifier/psyche", + "operation": "add", + "value": 1 + } + ] } ] }, @@ -7144,151 +7135,14 @@ ] }, "7ii1ig85j7a1gacorzkn6oyjdt3w6jzh": { - "description": "Choisissez une compétence. Si vous faites 6 ou moins à votre jet, vous considérez que votre jet est un 6. *Ne fonctionne pas sur les jets de fabrications et les jets d'œuvres*", + "description": "Choisissez une compétence. Si vous faites 6 ou moins à votre jet, vous considérez que votre jet est un 6. *Ne fonctionne pas sur les jets de fabrications et les jets d'œuvres* #todo", "id": "7ii1ig85j7a1gacorzkn6oyjdt3w6jzh", "effect": [ { "id": "v0lf1gwsairuei43r3u3eyc5v57segtc", "category": "choice", "text": "Vous ne pouvez pas faire moins de 6 sur vos jets de ", - "options": [ - { - "id": "cr0vgpzpca3gcjfiqfq4yy4ta14n6fjr", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "34ghl5vv2rwmr0zxznqee4wld2ng9q94", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "xceir7gw2atr8om3dt89jbjjihijbhre", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "0sg0evb589nlihsoleqvchdp5orqx9dv", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "7sdozpx5qmrisc7wcxule5w7fk931rxi", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "z482fmbjy7i5r1g51ie223392rb161gi", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "nd02a9m53ypyk3jen1mzt1nn9spj92dx", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "g3h2igg57rxpchnyg53jxk64vggzjcs4", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "hdeskp2s6zlw4xcfcaal5svo907bozhc", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "i9ie2gpcmn2pzxzz2dx4n0il92n68s6o", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "804h26knc7eh06s1t94x5gfg9i57sikz", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "luf7aprc50wr15wdrt1w1mimj3wgp2f6", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "a9ie2rutp48b0zjfvrjmz4rff3r7ujqi", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "rxrlyyf2azjs04fg4lno6oaao3fqwp2l", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "zw6ifv9b5a62asi43p8jtll8y34tfihq", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "ugxqcqnsl6u76tbv1hl0nvykeabp3a5f", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "dxpo0dq6qy48n1plte8ii2n7z4pg6tlw", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - } - ] + "options": [] } ] }, @@ -7343,52 +7197,80 @@ "options": [ { "text": "Modifieur de force", - "category": "value", - "property": "modifier/strength", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/strength", + "operation": "add", + "value": 1 + } + ] }, { "text": "Modifieur de dextérité", - "category": "value", - "property": "modifier/dexterity", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/dexterity", + "operation": "add", + "value": 1 + } + ] }, { "text": "Modifieur de constitution", - "category": "value", - "property": "modifier/constitution", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/constitution", + "operation": "add", + "value": 1 + } + ] }, { "text": "Modifieur d'intelligence", - "category": "value", - "property": "modifier/intelligence", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/intelligence", + "operation": "add", + "value": 1 + } + ] }, { "text": "Modifieur de curiosité", - "category": "value", - "property": "modifier/curiosity", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/curiosity", + "operation": "add", + "value": 1 + } + ] }, { "text": "Modifieur de charisme", - "category": "value", - "property": "modifier/charisma", - "operation": "add", - "value": 1 + "effects": [ + { + "category": "value", + "property": "modifier/charisma", + "operation": "add", + "value": 1 + } + ] }, { "text": "Modifieur de psyché", - "egory": "value", - "property": "modifier/psyche", - "operation": "add", - "value": 1 + "effects": [ + { + "egory": "value", + "property": "modifier/psyche", + "operation": "add", + "value": 1 + } + ] } ] } @@ -8225,47 +8107,14 @@ ] }, "s5kidncgfzw85ffubl718lx2f68suhqf": { - "description": "Votre connexion innée avec la magie vous a bénie d'un don pour cet art. Choisissez une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]]. Vous gagnez le premier niveau de cette branche.", + "description": "Votre connexion innée avec la magie vous a bénie d'un don pour cet art. Choisissez une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]]. Vous gagnez le premier niveau de cette branche. #todo", "id": "s5kidncgfzw85ffubl718lx2f68suhqf", "effect": [ { "id": "yjc9xk64ygtc5tugluia031nhxgxvi6z", "category": "choice", "text": "Vous gagnez le premier niveau de la branche de ", - "options": [ - { - "id": "l8b2fdpvjpihjeaqj1rs3el5w5jj0zww", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "xxscipqcvk2q5q97a3c926t523x6awth", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "i413jzkxi0tdjjj1aaz6m0bkm94uqahf", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "82pzzioqy1whd59xfdaiw88osvqqbqjw", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - } - ] + "options": [] }, { "id": "pzgqz28pnupmfmcf6mc7wmuhry775f7f", @@ -8394,47 +8243,14 @@ ] }, "qf3eru17f8u3hysq56k246mlq7p2rbc9": { - "description": "Vous gagnez un niveau dans une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]] dans laquelle vous avez déjà au moins un niveau.", + "description": "Vous gagnez un niveau dans une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]] dans laquelle vous avez déjà au moins un niveau. #todo", "id": "qf3eru17f8u3hysq56k246mlq7p2rbc9", "effect": [ { "id": "460k5ti0iesdfc8j4mlh6nrdrzg67g6f", "category": "choice", "text": "Vous gagnez un niveau dans la branche de ", - "options": [ - { - "id": "l8b2fdpvjpihjeaqj1rs3el5w5jj0zww", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "xxscipqcvk2q5q97a3c926t523x6awth", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "i413jzkxi0tdjjj1aaz6m0bkm94uqahf", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "82pzzioqy1whd59xfdaiw88osvqqbqjw", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - } - ] + "options": [] }, { "id": "8qddimnu5vwleys9fjoq84ju3d09ejpq", @@ -8563,47 +8379,14 @@ ] }, "sw45zzv7bf6v35h064f6zhcj1e7xbbr5": { - "description": "Vous gagnez un niveau dans une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]] dans laquelle vous avez déjà au moins un niveau.", + "description": "Vous gagnez un niveau dans une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]] dans laquelle vous avez déjà au moins un niveau. #todo", "id": "sw45zzv7bf6v35h064f6zhcj1e7xbbr5", "effect": [ { "id": "pbpmdu5tgvi1saopqseq5mj7qqml3z3k", "category": "choice", "text": "Vous gagnez un niveau dans la branche de ", - "options": [ - { - "id": "l8b2fdpvjpihjeaqj1rs3el5w5jj0zww", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "xxscipqcvk2q5q97a3c926t523x6awth", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "i413jzkxi0tdjjj1aaz6m0bkm94uqahf", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - }, - { - "id": "82pzzioqy1whd59xfdaiw88osvqqbqjw", - "category": "value", - "text": "", - "operation": "add", - "property": "", - "value": 0 - } - ] + "options": [] }, { "id": "qpq7g3m86jfpaopm1jofyfz6j69wk2nq", @@ -9262,52 +9045,80 @@ "options": [ { "text": "Force", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/strength" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/strength" + } + ] }, { "text": "Dextérité", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/dexterity" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/dexterity" + } + ] }, { "text": "Constitution", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/constitution" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/constitution" + } + ] }, { "text": "Intelligence", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/intelligence" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/intelligence" + } + ] }, { "text": "Curiosité", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/curiosity" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/curiosity" + } + ] }, { "text": "Charisme", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/charisma" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/charisma" + } + ] }, { "text": "Psyché", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/psyche" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/psyche" + } + ] } ] }, @@ -9910,52 +9721,80 @@ "options": [ { "text": "Force", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/strength" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/strength" + } + ] }, { "text": "Dextérité", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/dexterity" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/dexterity" + } + ] }, { "text": "Constitution", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/constitution" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/constitution" + } + ] }, { "text": "Intelligence", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/intelligence" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/intelligence" + } + ] }, { "text": "Curiosité", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/curiosity" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/curiosity" + } + ] }, { "text": "Charisme", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/charisma" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/charisma" + } + ] }, { "text": "Psyché", - "category": "value", - "operation": "add", - "value": 1, - "property": "modifier/psyche" + "effects": [ + { + "category": "value", + "operation": "add", + "value": 1, + "property": "modifier/psyche" + } + ] } ], "text": "+1 au modifieur de " @@ -9992,12 +9831,12 @@ }, "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij": { "id": "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij", - "description": "", + "description": "le Temps des Tempetes", "effect": [] }, "pfzopr4oyrsgxg0cbva16zzzly3kke9z": { "id": "pfzopr4oyrsgxg0cbva16zzzly3kke9z", - "description": "", + "description": "pour Audible", "effect": [] }, "fk0wmg94tlq78khq8zot2o5u4nnxr2gb": { diff --git a/shared/character.util.ts b/shared/character.util.ts index 7b64b7e..fd6d404 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -1,12 +1,14 @@ import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel } from "~/types/character"; import { z } from "zod/v4"; import characterConfig from '#shared/character-config.json'; -import { fakeA } from "#shared/proses"; -import { button, input, loading, numberpicker, select, Toaster, toggle } from "#shared/components.util"; +import proses, { preview } from "#shared/proses"; +import { button, buttongroup, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util"; import { div, dom, icon, text } from "#shared/dom.util"; -import { followermenu, tooltip } from "#shared/floating.util"; +import { followermenu, fullblocker, tooltip } from "#shared/floating.util"; import { clamp } from "#shared/general.util"; -import markdownUtil from "#shared/markdown.util"; +import markdown from "#shared/markdown.util"; +import { getText } from "./i18n"; +import type { User } from "~/types/auth"; const config = characterConfig as CharacterConfig; @@ -339,7 +341,7 @@ export class CharacterCompiler const choice = this._character.choices[feature.id]; if(choice) - choice.forEach(e => this.apply(feature.options[e]!)); + choice.forEach(e => feature.options[e]!.effects.forEach(this.apply.bind(this))); return; default: @@ -373,7 +375,7 @@ export class CharacterCompiler const choice = this._character.choices[feature.id]; if(choice) - choice.forEach(e => this.undo(feature.options[e]!)); + choice.forEach(e => feature.options[e]!.effects.forEach(this.undo.bind(this))); return; default: @@ -726,6 +728,7 @@ class PeoplePicker extends BuilderTab this._options = Object.values(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: () => { this._builder.character.people = people.id; + this._builder.character = { ...this._builder.character, people: people.id }; "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.forEach(f => f?.classList.toggle(e, false))); "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options[i]?.classList.toggle(e, true)); } @@ -880,7 +883,7 @@ class TrainingPicker extends BuilderTab return 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 relative"], listeners: { click: e => { this._builder.toggleTrainingOption(stat, parseInt(level[0]) as TrainingLevel, j); this.update(); - }}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }), choice ]); + }}}, [ markdown(config.features[option]!.description, undefined, { tags: { a: preview } }), choice ]); })) ]); } @@ -1135,4 +1138,376 @@ class AspectPicker extends BuilderTab return true; } +} + +export class CharacterSheet +{ + character?: CharacterCompiler; + container: HTMLElement = div(); + constructor(id: string, user: ComputedRef) + { + const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]); + this.container.replaceChildren(load); + useRequestFetch()(`/api/character/${id}`).then(character => { + if(character) + { + this.character = new CharacterCompiler(character); + + document.title = `d[any] - ${character.name}`; + load.remove(); + + this.render(); + } + else + { + //ERROR + } + }); + } + render() + { + if(!this.character) + return; + + const character = this.character.compiled; + console.log(character); + this.container.replaceChildren(div('flex flex-col justify-center gap-1', [ + div("flex flex-row gap-4 justify-between", [ + div(), + + div("flex lg:flex-row flex-col gap-6 items-center justify-center", [ + div("flex gap-6 items-center", [ + div('inline-flex select-none items-center justify-center overflow-hidden align-middle h-16', [ + div('text-light-100 dark:text-dark-100 leading-1 flex p-4 items-center justify-center bg-light-25 dark:bg-dark-25 font-medium', [ + icon("radix-icons:person", { width: 16, height: 16 }), + ]) + ]), + + div("flex flex-col", [ + dom("span", { class: "text-xl font-bold", text: character.name }), + dom("span", { class: "text-sm", text: `De ${character.username}` }) + ]), + + div("flex flex-col", [ + dom("span", { class: "font-bold", text: `Niveau ${character.level}` }), + dom("span", { text: config.peoples[character.race]?.name ?? 'Peuple inconnu' }) + ]) + ]), + + div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [ + dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [ + text("PV: "), + dom("span", { + class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35", + text: `${character.health - character.variables.health}` + }), + text(`/ ${character.health}`) + ]), + dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [ + text("Mana: "), + dom("span", { + class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35", + text: `${character.mana - character.variables.mana}` + }), + text(`/ ${character.mana}`) + ]) + ]) + ]), + + div("self-center", [ + /* user && user.id === character.owner ? + button(icon("radix-icons:pencil-2"), () => { + }, "icon") + : div() */ + ]) + ]), + + div("flex flex-row justify-center 2xl:gap-4 gap-2 p-4 border-b border-light-35 dark:border-dark-35", [ + div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [ + div("flex flex-col items-center px-2", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.strength}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Force" }) + ]), + div("flex flex-col items-center px-2", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.dexterity}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Dextérité" }) + ]), + div("flex flex-col items-center px-2", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.constitution}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Constitution" }) + ]), + div("flex flex-col items-center px-2", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.intelligence}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Intelligence" }) + ]), + div("flex flex-col items-center px-2", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.curiosity}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Curiosité" }) + ]), + div("flex flex-col items-center px-2", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.charisma}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Charisme" }) + ]), + div("flex flex-col items-center px-2", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.psyche}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Psyché" }) + ]) + ]), + + div('border-l border-light-35 dark:border-dark-35'), + + div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [ + div("flex flex-col px-2 items-center", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.initiative}` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Initiative" }) + ]), + div("flex flex-col px-2 items-center", [ + dom("span", { class: "2xl:text-2xl text-xl font-bold", text: character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }), + dom("span", { class: "text-sm 2xl:text-base", text: "Course" }) + ]) + ]), + + div('border-l border-light-35 dark:border-dark-35'), + + div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [ + icon("game-icons:checked-shield", { width: 32, height: 32 }), + div("flex flex-col px-2 items-center", [ + dom("span", { + class: "2xl:text-2xl text-xl font-bold", + text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap)}` + }), + dom("span", { class: "text-sm 2xl:text-base", text: "Passive" }) + ]), + div("flex flex-col px-2 items-center", [ + dom("span", { + class: "2xl:text-2xl text-xl font-bold", + text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap)}` + }), + dom("span", { class: "text-sm 2xl:text-base", text: "Blocage" }) + ]), + div("flex flex-col px-2 items-center", [ + dom("span", { + class: "2xl:text-2xl text-xl font-bold", + text: `${clamp(character.defense.static + character.defense.activedodge + character.defense.passiveparry, 0, character.defense.hardcap)}` + }), + dom("span", { class: "text-sm 2xl:text-base", text: "Esquive" }) + ]) + ]), + ]), + + div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4", [ + div("flex flex-col gap-4 py-1 w-80", [ + div("flex flex-col py-1 gap-4", [ + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") + ]), + + div("grid grid-cols-3 gap-2", + Object.entries(character.abilities).map(([ability, value]) => + div("flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70", [ + dom("span", { class: "font-bold text-base text-light-100 dark:text-dark-100", text: `+${value}` }), + dom("span", { text: abilityTexts[ability as Ability] || ability }) + ]) + ) + ), + + + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") + ]), + + character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles' }) : undefined, + character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes' }) : undefined, + character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées' }) : undefined, + character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes' }) : undefined, + character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains' }) : undefined, + character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables' }) : undefined, + character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles' }) : undefined, + character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues' }) : undefined, + character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers' }) : undefined, + character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains' }) : undefined, + ]) : undefined, + + character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ + character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères' }) : undefined, + character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures' }) : undefined, + character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes' }) : undefined, + ]) : undefined, + + div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ + div('flex flex-row items-center gap-2', [ text('Précision'), dom('span', { text: character.spellranks.precision.toString(), class: 'font-bold' }) ]), + div('flex flex-row items-center gap-2', [ text('Savoir'), dom('span', { text: character.spellranks.knowledge.toString(), class: 'font-bold' }) ]), + div('flex flex-row items-center gap-2', [ text('Instinct'), dom('span', { text: character.spellranks.instinct.toString(), class: 'font-bold' }) ]), + div('flex flex-row items-center gap-2', [ text('Oeuvres'), dom('span', { text: character.spellranks.arts.toString(), class: 'font-bold' }) ]), + ]) + ]) + ]), + + div('border-l border-light-35 dark:border-dark-35'), + + tabgroup([ + { id: 'actions', title: [ text('Actions') ], content: () => [ + div('flex flex-col gap-8', [ + div('flex flex-col gap-2', [ + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), + div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), + ]), + + div('flex flex-col gap-2', [ + div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), + ...(character.lists.action?.map(e => div('flex flex-col gap-1', [ + //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]), + markdown(getText(e), undefined, { tags: { a: preview } }), + ])) ?? []) + ]), + ]), + div('flex flex-col gap-2', [ + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), + div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), + ]), + + div('flex flex-col gap-2', [ + div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), + ...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [ + //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]), + markdown(getText(e), undefined, { tags: { a: preview } }), + ])) ?? []) + ]), + ]), + div('flex flex-col gap-2', [ + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), + ]), + + div('flex flex-col gap-2', [ + div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), + ...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [ + //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]), + markdown(getText(e), undefined, { tags: { a: preview } }), + ])) ?? []) + ]), + ]), + ]), + ] }, + + { id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) }, + + { id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) }, + + { id: 'inventory', title: [ text('Inventaire') ], content: () => [ + + ] }, + + { id: 'notes', title: [ text('Notes') ], content: () => [ + + ] }, + ], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } }), + ]) + ])); + } + abilitiesTab(character: CompiledCharacter) + { + return [ + div('flex flex-col gap-2', [ + ...(character.lists.passive?.map(e => div('flex flex-col gap-1', [ + //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]), + markdown(getText(e), undefined, { tags: { a: preview } }), + ])) ?? []), + ]), + ]; + } + spellTab(character: CompiledCharacter) + { + return [ + div('flex flex-col gap-2', [ + div('flex flex-row justify-between items-center', [ + div('flex flex-row gap-2 items-center', [ + dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }), + buttongroup([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: 'rank', class: { option: 'px-2 py-1 text-sm' } }), + ]) + ]) + ]) + ] + } + spellPanel() + { + if(!this.character) + return; + + const character = this.character.compiled; + const availableSpells = Object.values(config.spells).filter(spell => { + if (spell.rank === 4) return false; + if (character.spellranks[spell.type] < spell.rank) return false; + return true; + }); + + const textAmount = text(character.variables.spells.length.toString()), textMax = text(character.spellslots.toString()); + const container = div("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-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [ + div("flex flex-row justify-between items-center mb-4", [ + dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }), + div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ textAmount, text(' / '), textMax, text(' sorts maitrisés') ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => { + setTimeout(blocker.close, 150); + container.setAttribute('data-state', 'inactive'); + }, "p-1"), "Fermer", "left") ]) + ]), + div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', availableSpells.map(spell => { + let state = character.lists.spells?.includes(spell.id) ? 'given' : character.variables.spells.includes(spell.id) ? 'choosen' : 'empty'; + const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => { + if(state === 'choosen') + { + //this.character.variable('spells', character.variables.spells.filter(e => e !== spell.id)); //TO REWORK + state = 'empty'; + } + else if(state === 'empty') + { + //this.character.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK + state = 'choosen'; + } + //character = compiler.compiled; //TO REWORK + toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'; + textAmount.textContent = character.variables.spells.length.toString(); + }, "px-2 py-1 text-sm font-normal"); + toggleButton.disabled = state === 'given'; + return foldable(() => [ + markdown(spell.effect), + ], [ div("flex flex-row justify-between gap-2", [ + dom("span", { class: "text-lg font-bold", text: spell.name }), + div("flex flex-row items-center gap-6", [ + div("flex flex-row text-sm gap-2", + spell.elements.map(el => + dom("span", { + class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[el].class], + text: elementTexts[el].text + }) + ) + ), + div("flex flex-row text-sm gap-1", [ + ...(spell.rank !== 4 ? [ + dom("span", { text: `Rang ${spell.rank}` }), + text("/"), + dom("span", { text: spellTypeTexts[spell.type] }), + text("/") + ] : []), + dom("span", { text: `${spell.cost} mana` }), + text("/"), + dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` }) + ]), + toggleButton, + ]), + ]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } }); + })) + ]); + const blocker = fullblocker([ container ], { closeWhenOutside: true }); + setTimeout(() => container.setAttribute('data-state', 'active'), 1); + } } \ No newline at end of file diff --git a/shared/components.util.ts b/shared/components.util.ts index 84565f3..4c71b43 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -48,6 +48,26 @@ export function button(content: Node, onClick?: () => void, cls?: Class) }) return btn; } +export function buttongroup(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean }) +{ + let currentValue = settings?.value; + const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 dark:hover:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-none + border border-light-40 dark:border-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[selected]:z-10 data-[selected]:border-light-50 dark:data-[selected]:border-dark-50 + data-[selected]:shadow-raw transition-[box-shadow] data-[selected]:shadow-light-50 dark:data-[selected]:shadow-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40`, + settings?.class?.option], text: e.text, attributes: { 'data-selected': settings?.value === e.value }, listeners: { click: function() { + if(currentValue !== e.value) + { + elements.forEach(e => e.toggleAttribute('data-selected', false)); + this.toggleAttribute('data-selected', true); + + if(!settings?.onChange || settings?.onChange(e.value)) + { + currentValue = e.value; + } + } + }}})) + return div(['flex flex-row', settings?.class?.container], elements); +} export type Option = { text: string, render?: () => HTMLElement, value: T | Option[] } | undefined; type StoredOption = { item: Option, dom: HTMLElement, container?: HTMLElement, children?: Array> }; export function select>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement @@ -434,6 +454,26 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo }, [ 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; } +export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } }) +{ + const focus = settings?.focused ?? tabs[0]?.id; + const titles = tabs.map((e, i) => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() { + if(this.hasAttribute('data-focus')) + return; + + titles.forEach(e => e.toggleAttribute('data-focus', false)); + this.toggleAttribute('data-focus', true); + const _content = typeof e.content === 'function' ? e.content() : e.content; + //@ts-expect-error + content.replaceChildren(..._content); + }}}, e.title)); + const _content = tabs.find(e => e.id === focus)?.content; + const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content); + return div(['flex flex-col', settings?.class?.container], [ + div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles), + content + ]); +} export interface ToastConfig { diff --git a/shared/dom.util.ts b/shared/dom.util.ts index 7749848..62e914f 100644 --- a/shared/dom.util.ts +++ b/shared/dom.util.ts @@ -4,9 +4,9 @@ export type Node = HTMLElement | SVGElement | Text | undefined; export type NodeChildren = Array; export type Class = string | Array | Record | undefined; -type Listener = | ((ev: HTMLElementEventMap[K]) => any) | { +type Listener = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | { options?: boolean | AddEventListenerOptions; - listener: (ev: HTMLElementEventMap[K]) => any; + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any; } | undefined; export interface NodeProperties @@ -42,9 +42,9 @@ export function dom(tag: K, properties?: { const key = k as keyof HTMLElementEventMap, value = v as Listener; if(typeof value === 'function') - element.addEventListener(key, value); + element.addEventListener(key, value.bind(element)); else if(value) - element.addEventListener(key, value.listener, value.options); + element.addEventListener(key, value.listener.bind(element), value.options); } } @@ -56,6 +56,10 @@ export function div(cls?: Class, children?: NodeChildren): HTMLDivElement { return dom("div", { class: cls }, children); } +export function span(cls?: Class, children?: NodeChildren): HTMLSpanElement +{ + return dom("span", { class: cls }, children); +} export function svg(tag: K, properties?: NodeProperties, children?: Omit): SVGElementTagNameMap[K] { const element = document.createElementNS("http://www.w3.org/2000/svg", tag); diff --git a/shared/feature.util.ts b/shared/feature.util.ts index a6f7925..ce0b6c0 100644 --- a/shared/feature.util.ts +++ b/shared/feature.util.ts @@ -1,7 +1,7 @@ import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } 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 { preview } from "#shared/proses"; import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util"; import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util"; import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util"; @@ -65,7 +65,7 @@ export class HomebrewBuilder const promise: Promise = this._editor.edit(feature).then(f => { this._config.features[feature.id] = f; return f; - }).catch(() => feature).finally(() => { + }).catch((e) => { if(e) console.error(e); return feature; }).finally(() => { setTimeout(popup.close, 150); this._editor.container.setAttribute('data-state', 'inactive'); }); @@ -133,7 +133,7 @@ class PeopleEditor extends BuilderTab 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 => { this._builder.edit(config.features[feature]!).then(e => { - element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } })); + element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } })); }); }, contextmenu: (e) => { e.preventDefault(); @@ -154,7 +154,7 @@ class PeopleEditor extends BuilderTab } }) } } }, [ text('Supprimer') ]) : undefined, ], { placement: "right-start", priority: false }); - }}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]); + }}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]); return element; } const peopleRender = (people: RaceConfig) => { @@ -180,7 +180,7 @@ class TrainingEditor extends BuilderTab 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 => { this._builder.edit(config.features[feature]!).then(e => { - element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } })); + element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } })); }); }, contextmenu: (e) => { e.preventDefault(); @@ -201,7 +201,7 @@ class TrainingEditor extends BuilderTab } }) } } }, [ text('Supprimer') ]) : undefined, ], { placement: "right-start", priority: false }); - }}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]); + }}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]); return element; }; const statRenderBlock = (stat: MainStat) => { @@ -411,7 +411,7 @@ export class FeatureEditor private _renderEffect(effect: Partial): 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('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: preview } }) ]), div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => { content.replaceWith(this._edit(effect)); }, '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'), () => { @@ -503,14 +503,15 @@ export class FeatureEditor break; case 'choice': const add = () => { - const option: Extract["options"][number] = { id: getID(), category: 'value', text: '', operation: 'add', property: '', value: 0 }; - (buffer as Extract).options.push(option); + const option: { text: string; effects: (Partial)[]; } = { effects: [{ id: getID() }], text: '' }; + (buffer as FeatureChoice).options.push(option as FeatureChoice["options"][number]); list.appendChild(render(option, true)); }; - const render = (option: FeatureEffect & { text: string }, state: boolean): HTMLElement => { - const { top: _top, bottom: _bottom } = drawByCategory(option); + const render = (option: { text: string; effects: (Partial)[]; }, state: boolean): HTMLElement => { + + /* const { top: _top, bottom: _bottom } = drawByCategory(option); const combo = combobox([...featureChoices].filter(e => (e?.value as FeatureItem)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureItem)?.category !== 'choice') : e.value; return e; }), { defaultValue: match(option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => { - option = { id: option.id, ...e } as FeatureEffect & { text: string }; + option = { id: option.id, ...e } as { text: string; effects: (Partial)[]; }; const element = render(option, true); _content.replaceWith(element); _content = element; @@ -519,7 +520,7 @@ export class FeatureEditor _content.remove(); (buffer as Extract).options = (buffer as Extract).options.filter(e => e.id !== option.id); }, '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; + 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 => render(e, false)) ?? []); 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' }), tooltip(button(icon('radix-icons:plus'), () => add(), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Ajouter une option', 'bottom') ]; @@ -590,42 +591,42 @@ const featureChoices: Option>[] = [ ] }, { 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: abilityTexts[e as Ability], value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) } + { 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: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } }, - { text: 'Modifieur de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } }, - { text: 'Modifieur de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } }, - { text: 'Modifieur d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } }, - { text: 'Modifieur de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } }, - { text: 'Modifieur de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } }, - { text: 'Modifieur de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } }, - { text: 'Modifieur au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [ - { text: 'Modifieur de force', effects: [ { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } ] }, - { text: 'Modifieur de dextérité', effects: [ { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } ] }, - { text: 'Modifieur de constitution', effects: [ { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } ] }, - { text: 'Modifieur d\'intelligence', effects: [ { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } ] }, - { text: 'Modifieur de curiosité', effects: [ { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } ] }, - { text: 'Modifieur de charisme', effects: [ { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } ] }, - { text: 'Modifieur de psyché', effects: [ { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } ] } + { 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: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } }, - { text: 'Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } }, - { text: 'Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 } }, - { text: 'Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 } }, - { text: 'Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 } }, - { text: 'Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } }, - { text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } }, + { 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: 'Force', effects: [{ category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 }] }, - { text: 'Dextérité', effects: [{ category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 }] }, - { text: 'Constitution', effects: [{ category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 }] }, - { text: 'Intelligence', effects: [{ category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 }] }, - { text: 'Curiosité', effects: [{ category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 }] }, - { text: 'Charisme', effects: [{ category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 }] }, - { text: 'Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }] } + { text: 'Résistance > Force', effects: [{ category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 }] }, + { 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', value: RESISTANCES.map(e => ({ text: resistanceTexts[e as Resistance], value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) }, diff --git a/shared/proses.ts b/shared/proses.ts index 39471d8..7e8914e 100644 --- a/shared/proses.ts +++ b/shared/proses.ts @@ -20,8 +20,8 @@ export const a: Prose = { const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link); - const el = dom('a', { class: 'text-accent-blue inline-flex items-center', attributes: { href: nav.href }, listeners: { - 'click': (e) => { + const el = dom('a', { class: ['text-accent-blue inline-flex items-center', properties?.class], attributes: { href: nav.href }, listeners: { + 'click': (e) => { e.preventDefault(); router.push(link); } @@ -45,7 +45,7 @@ export const a: Prose = { return [async('large', Content.getContent(overview.id).then((_content) => { if(_content?.type === 'markdown') { - return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }); + return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); } if(_content?.type === 'canvas') { @@ -63,7 +63,7 @@ export const a: Prose = { return el; } } -export const fakeA: Prose = { +export const preview: Prose = { custom(properties, children) { const href = properties.href as string; const { hash, pathname } = parseURL(href); @@ -71,11 +71,9 @@ export const fakeA: Prose = { const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname); - const el = dom('span', { class: 'cursor-pointer text-accent-blue inline-flex items-center' }, [ - dom('span', {}, [ - ...(children ?? []), - overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined - ]) + const el = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [ + ...(children ?? []), + overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined ]); @@ -93,7 +91,7 @@ export const fakeA: Prose = { return [async('large', Content.getContent(overview.id).then((_content) => { if(_content?.type === 'markdown') { - return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }); + return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); } if(_content?.type === 'canvas') { diff --git a/types/character.d.ts b/types/character.d.ts index 86c49a6..e983412 100644 --- a/types/character.d.ts +++ b/types/character.d.ts @@ -1,4 +1,5 @@ import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES } from "#shared/character.util"; +import type { Localized } from "#shared/general"; export type MainStat = typeof MAIN_STATS[number]; export type Ability = typeof ABILITIES[number]; @@ -13,14 +14,22 @@ export type Resistance = typeof RESISTANCES[number]; export type FeatureID = string; export type i18nID = string; +export type RecursiveKeyOf = { + [TKey in keyof TObj & (string | number)]: + TObj[TKey] extends any[] ? `${TKey}` : + TObj[TKey] extends object + ? `${TKey}` | `${TKey}/${RecursiveKeyOf}` + : `${TKey}`; +}[keyof TObj & (string | number)]; + export type Character = { id: number; - name: string; - people?: string; + name: string; //Free text + people?: string; //People ID level: number; aspect?: number; - notes?: string | null; + notes?: { public?: string, private?: string }; //Free text training: Record>>; leveling: Partial>; @@ -38,11 +47,12 @@ export type CharacterVariables = { exhaustion: number; sickness: Array<{ id: string, state: number | true }>; + poisons: Array<{ id: string, state: number | true }>; spells: string[]; //Spell ID items: ItemState[]; }; type ItemState = { - id: string, + id: string; amount: number; enchantments?: []; charges?: number; @@ -55,11 +65,16 @@ export type CharacterConfig = { spells: SpellConfig[]; aspects: AspectConfig[]; features: Record; - enchantments: Record; //TODO + enchantments: Record; //TODO items: Record; - lists: Record; + lists: Record }>; texts: Record; }; +export type EnchantementConfig = { + name: string; //TODO -> TextID + effect: Array; + power: number; +} export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig); type CommonItemConfig = { id: string; @@ -87,7 +102,7 @@ type WondrousConfig = { category: 'wondrous'; name: string; //TODO -> TextID description: i18nID; - effect: FeatureEffect[]; + effect: FeatureItem[]; }; type MundaneConfig = { category: 'mundane'; @@ -96,25 +111,25 @@ type MundaneConfig = { }; export type SpellConfig = { id: string; - name: string; + name: string; //TODO -> TextID rank: 1 | 2 | 3 | 4; type: SpellType; cost: number; speed: "action" | "reaction" | number; elements: Array; - effect: string; + effect: string; //TODO -> TextID concentration: boolean; tags?: string[]; }; export type RaceConfig = { id: string; - name: string; - description: string; + name: string; //TODO -> TextID + description: string; //TODO -> TextID options: Record; }; export type AspectConfig = { name: string; - description: string; + description: string; //TODO -> TextID stat: MainStat | 'special'; alignment: Alignment; magic: boolean; @@ -122,33 +137,42 @@ export type AspectConfig = { physic: { min: number, max: number }; mental: { min: number, max: number }; personality: { min: number, max: number }; - options: FeatureEffect[]; + options: FeatureItem[]; }; -export type FeatureEffect = { +export type FeatureValue = { id: FeatureID; category: "value"; operation: "add" | "set" | "min"; - property: string; + property: RecursiveKeyOf | 'spec' | 'ability' | 'training'; value: number | `modifier/${MainStat}` | false; -} | { +} +export type FeatureEquipment = { + id: FeatureID; + category: "value"; + operation: "add" | "set" | "min"; + property: 'weapon/damage' | 'armor/health' | 'armor/absorb/flat' | 'armor/absorb/percent'; + value: number | `modifier/${MainStat}` | false; +} +export type FeatureList = { id: FeatureID; category: "list"; list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive"; action: "add" | "remove"; - item: string; + item: string | i18nID; extra?: any; }; -export type FeatureItem = FeatureEffect | { +export type FeatureChoice = { id: FeatureID; category: "choice"; - text: string; + text: string; //TODO -> TextID settings?: { //If undefined, amount is 1 by default amount: number; exclusive: boolean; //Disallow to pick the same option twice }; - options: Array; + options: Array<{ text: string, effects: Array }>; //TODO -> TextID }; +export type FeatureItem = FeatureValue | FeatureList | FeatureChoice; export type Feature = { id: FeatureID; description: i18nID; @@ -199,13 +223,16 @@ export type CompiledCharacter = { magicinstinct: number; }; - bonus: Record; //Any special bonus goes here + bonus: { + defense: Partial>; + abilities: Partial>; + }; //Any special bonus goes here resistance: Record; modifier: Record; abilities: Partial>; level: number; - lists: { [K in Extract["list"]]?: string[] }; + lists: { [K in FeatureList['list']]?: string[] }; //string => ListItem ID - notes: string; + notes: { public: string, private: string }; }; \ No newline at end of file From 61d2d144b7f5aa887c1fd5c70e41cb24f7f88eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Tue, 30 Sep 2025 17:15:49 +0200 Subject: [PATCH 3/4] Spell UI, variables saving and mail server fixes (finally working in prod !!!) --- db.sqlite | Bin 761856 -> 761856 bytes db.sqlite-shm | Bin 32768 -> 32768 bytes db.sqlite-wal | Bin 8272 -> 0 bytes db/schema.ts | 2 +- server/api/admin/jobs/[id].post.ts | 17 +- server/api/character/[id]/values.get.ts | 35 -- .../{values.post.ts => variables.post.ts} | 13 +- server/components/mail/base.vue | 6 +- server/tasks/mail.ts | 97 +++--- shared/character-config.json | 16 +- shared/character.util.ts | 300 +++++++++++------- shared/components.util.ts | 34 +- shared/dom.util.ts | 6 +- shared/floating.util.ts | 9 +- shared/proses.ts | 68 ++-- types/character.d.ts | 2 +- 16 files changed, 336 insertions(+), 269 deletions(-) delete mode 100644 server/api/character/[id]/values.get.ts rename server/api/character/[id]/{values.post.ts => variables.post.ts} (74%) diff --git a/db.sqlite b/db.sqlite index 827b942840d0381ee4ff23b27394b0c04119c9aa..e7966d7a1dc347b7e88df2b2ac9023a7a01a40c3 100644 GIT binary patch delta 660 zcmZ9KL2J}N6vwk$wXNM+5Co;5l5^S3B$Lg=lU@ZsfT*-dHj`u~lSwj@O(r0_6`|q> zaHU@S0)i;mYtQ1vv-nY3oV|z$@5dXMA3S*fw>%#&&&QuH4mW;%fBNO<;@-NyT7Fv& z1|N6!urVCH({mPGRB&SSyvNamBb|oO`yPAy%bH%0vA>PnDfwSv4U?1~hWHn%DnAq0J4BQF3Va!H#@tlv(V>XO^~lgr6av!S`7xvIIQ b**v?PY<-vvwogtEw>0a1{p|E`2S5A+f12Kn delta 211 zcmZoTpx1CfZ^Pes=3A@IZc6wo52BkIwly#^F5u?-!N9~k(G~! zL6p;%k%2*)p;3MM13jjM>Aw0*GCb7`l0acbUa;^|9VRcsMlD_*gkZK$X-Z~EMyi>a zQLdq7p{b=oc3vX~vI_O-Ky69WqxG4%xOJ3L3rjN#a#Qn4+JEXZ0WmWWvj8zG5VHX> c`}Uvu91HXrIofB%gD@u$b8Vj$&zDz^o@1e@vP+ni9o2#n7F1^DuXok5?9lhVApsVg7u+ zxp*|+{GaFfuB|C+ADuua&T-ZG3oy)t}AAh^#WX zf<-R@Lk$pwwLq>&r!7S))NY@+!|G$4TPi2DKrq#FZd(Xqwgwa?C$sRv3TihWoMH9e z*%-K79|(aZ0Wfl$H`4Mz^zh4IgnOq@D5EKqwBmT3aA850Elj?nhfERJ(NaC9B_KcU)!OjzCDs z1WnBl6Ge~|T!>a{LfLi%I78^`Ea;%>-S{=yK7ZY-I;I9R!mdu+m`&B6%|H zZuy4QOpp&x)`hnYf47Pp5<+l5d^TOKDJzog%BlJ4!Kq7cQ>f8Y5Q#lOJ_z7t%8+9c z*(FXyx;?(XhjZ`b_rH6AFZUh#{P5Zz=co3U7F)w&Yw;^j`{)EZfliI)OKlz{<8A!#K{?Pkg$&EuVUSzO-v;2DktK diff --git a/db/schema.ts b/db/schema.ts index 4a24196..2640c65 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -55,7 +55,7 @@ export const characterTable = table("character", { owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }), people: text().notNull(), level: int().notNull().default(1), - variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"equipment": [],"exhaustion": 0,"sickness": []}'), + variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"equipment": [],"exhaustion": 0,"sickness": [],"poisons": []}'), aspect: int(), notes: text(), diff --git a/server/api/admin/jobs/[id].post.ts b/server/api/admin/jobs/[id].post.ts index 2f21ded..3cc375b 100644 --- a/server/api/admin/jobs/[id].post.ts +++ b/server/api/admin/jobs/[id].post.ts @@ -4,7 +4,11 @@ declare module 'nitropack' { interface TaskPayload { - type: string + type: string; + } + interface TaskResult + { + error?: Error | string; } } @@ -17,7 +21,7 @@ export default defineEventHandler(async (e) => { return; } const id = getRouterParam(e, 'id'); - const payload: Record = await readBody(e); + const body: Record = await readBody(e); if(!id) { @@ -25,8 +29,11 @@ export default defineEventHandler(async (e) => { return; } - payload.type = id; - payload.data = JSON.parse(payload.data); + body.data = JSON.parse(body.data); + const payload = { + type: id, + data: body, + } const result = await runTask(id, { payload: payload @@ -36,7 +43,7 @@ export default defineEventHandler(async (e) => { { setResponseStatus(e, 500); - if(result.error && (result.error as Error).message) + if(result.error && result.error.message) throw result.error; else if(result.error) throw new Error(result.error); diff --git a/server/api/character/[id]/values.get.ts b/server/api/character/[id]/values.get.ts deleted file mode 100644 index 41d58b5..0000000 --- a/server/api/character/[id]/values.get.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { and, eq, sql } from 'drizzle-orm'; -import useDatabase from '~/composables/useDatabase'; -import { characterTable } from '~/db/schema'; -import type { CharacterVariables } from '~/types/character'; - -export default defineEventHandler(async (e) => { - const id = getRouterParam(e, "id"); - - if(!id) - { - setResponseStatus(e, 400); - return; - } - - const session = await getUserSession(e); - if(!session.user) - { - setResponseStatus(e, 401); - return; - } - - const db = useDatabase(); - const character = db.select({ - health: characterTable.health, - mana: characterTable.mana, - }).from(characterTable).where(and(eq(characterTable.id, parseInt(id, 10)), eq(characterTable.owner, session.user.id))).get(); - - if(character !== undefined) - { - return character as CharacterVariables; - } - - setResponseStatus(e, 404); - return; -}); \ No newline at end of file diff --git a/server/api/character/[id]/values.post.ts b/server/api/character/[id]/variables.post.ts similarity index 74% rename from server/api/character/[id]/values.post.ts rename to server/api/character/[id]/variables.post.ts index 35c7dc2..137a36c 100644 --- a/server/api/character/[id]/values.post.ts +++ b/server/api/character/[id]/variables.post.ts @@ -1,7 +1,8 @@ import { eq } from 'drizzle-orm'; import useDatabase from '~/composables/useDatabase'; import { characterTable } from '~/db/schema'; -import type { CharacterValues } from '~/types/character'; +import { CharacterVariablesValidation } from '~/shared/character.util'; +import type { CharacterVariables } from '~/types/character'; export default defineEventHandler(async (e) => { const id = getRouterParam(e, "id"); @@ -11,11 +12,12 @@ export default defineEventHandler(async (e) => { return; } - const body = await readBody(e) as CharacterValues; - if(!body) + const body = await readValidatedBody(e, CharacterVariablesValidation.safeParse); + if(!body.success) { + console.error(body.error); setResponseStatus(e, 400); - return; + throw body.error; } const db = useDatabase(); @@ -35,8 +37,7 @@ export default defineEventHandler(async (e) => { } db.update(characterTable).set({ - health: body.health, - mana: body.mana, + variables: body.data }).where(eq(characterTable.id, parseInt(id, 10))).run(); setResponseStatus(e, 200); diff --git a/server/components/mail/base.vue b/server/components/mail/base.vue index 0207356..f0067a5 100644 --- a/server/components/mail/base.vue +++ b/server/components/mail/base.vue @@ -1,9 +1,9 @@ \ No newline at end of file diff --git a/server/tasks/mail.ts b/server/tasks/mail.ts index 1a4e32d..754d661 100644 --- a/server/tasks/mail.ts +++ b/server/tasks/mail.ts @@ -17,10 +17,9 @@ export const templates: Record = { import type Mail from 'nodemailer/lib/mailer'; interface MailPayload { - type: 'mail' - to: string[] - template: string - data: Record + to: string[]; + template: string; + data: Record; } const transport = nodemailer.createTransport({ @@ -55,51 +54,59 @@ if(process.env.NODE_ENV === 'production') }); } -export default async function(e: TaskEvent) { - try { - if(e.payload.type !== 'mail') - { - throw new Error(`Données inconnues`); +export default defineTask({ + meta: { + name: 'mail', + description: '' + }, + run: async ({ payload, context }) => { + try { + if(payload.type !== 'mail') + { + throw new Error(`Données inconnues`); + } + + const mailPayload = payload.data as MailPayload; + const template = templates[mailPayload.template]; + + console.log(mailPayload); + + if(!template) + { + throw new Error(`Modèle de mail ${mailPayload.template} inconnu`); + } + + console.time('Generating HTML'); + const mail: Mail.Options = { + from: 'd[any] - Ne pas répondre ', + to: mailPayload.to, + html: await render(template.component, mailPayload.data), + subject: template.subject, + textEncoding: 'quoted-printable', + }; + console.timeEnd('Generating HTML'); + + if(mail.html === '') + return { result: false, error: new Error("Invalid content") }; + + console.time('Sending Mail'); + const status = await transport.sendMail(mail); + console.timeEnd('Sending Mail'); + + if(status.rejected.length > 0) + { + return { result: false, error: status.response, details: status.rejectedErrors }; + } + + return { result: true }; } - - const payload = e.payload as MailPayload; - const template = templates[payload.template]; - - if(!template) + catch(e) { - throw new Error(`Modèle de mail ${payload.template} inconnu`); + console.error(e); + return { result: false, error: e }; } - - console.time('Generating HTML'); - const mail: Mail.Options = { - from: 'd[any] - Ne pas répondre ', - to: payload.to, - html: await render(template.component, payload.data), - subject: template.subject, - textEncoding: 'quoted-printable', - }; - console.timeEnd('Generating HTML'); - - if(mail.html === '') - return { result: false, error: new Error("Invalid content") }; - - console.time('Sending Mail'); - const status = await transport.sendMail(mail); - console.timeEnd('Sending Mail'); - - if(status.rejected.length > 0) - { - return { result: false, error: status.response, details: status.rejectedErrors }; - } - - return { result: true }; } - catch(e) - { - console.error(e); - return { result: false, error: e }; - } -} +}) async function render(component: any, data: Record): Promise { diff --git a/shared/character-config.json b/shared/character-config.json index ccbf1df..8fdbb6a 100644 --- a/shared/character-config.json +++ b/shared/character-config.json @@ -558,12 +558,18 @@ } }, "lists": { - "sickness": [ - { - "id": "", - "name": "Pourriture mortelle" + "sickness": { + "id": "sickness", + "config": { + + }, + "values": { + "rotted": { + "id": "rotted", + "name": "Pourriture mortelle" + } } - ] + } }, "peoples": { "e662m19q590kn4dowvssowi1qf8ia7sk": { diff --git a/shared/character.util.ts b/shared/character.util.ts index fd6d404..90a2e87 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -1,9 +1,9 @@ -import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel } from "~/types/character"; +import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel } from "~/types/character"; import { z } from "zod/v4"; import characterConfig from '#shared/character-config.json'; import proses, { preview } from "#shared/proses"; import { button, buttongroup, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util"; -import { div, dom, icon, text } from "#shared/dom.util"; +import { div, dom, icon, span, text } from "#shared/dom.util"; import { followermenu, fullblocker, tooltip } from "#shared/floating.util"; import { clamp } from "#shared/general.util"; import markdown from "#shared/markdown.util"; @@ -40,6 +40,7 @@ export const defaultCharacter: Character = { items: [], exhaustion: 0, sickness: [], + poisons: [], }, owner: -1, @@ -113,7 +114,10 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c magicelement: 0, magicinstinct: 0, }, - bonus: {}, + bonus: { + abilities: {}, + defense: {}, + }, resistance: {}, initiative: 0, capacity: 0, @@ -125,7 +129,7 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c spells: [], }, aspect: "", - notes: character.notes ?? "", + notes: Object.assign({ public: '', private: '' }, character.notes), }); export const mainStatTexts: Record = { @@ -158,6 +162,10 @@ export const elementTexts: Record light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' }, psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' }, }; +export const elementDom = (element: SpellElement) => dom("span", { + class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class], + text: elementTexts[element].text +}); export const alignmentTexts: Record = { 'loyal_good': 'Loyal bon', @@ -205,6 +213,22 @@ export const resistanceTexts: Record = { 'instinct': 'Sorts d\'instinct', }; +export const CharacterVariablesValidation = z.object({ + health: z.number(), + mana: z.number(), + exhaustion: z.number(), + + sickness: z.array(z.object({ + id: z.string(), + state: z.number().min(1).max(7).or(z.literal(true)), + })), + poisons: z.array(z.object({ + id: z.string(), + state: z.number().min(1).max(7).or(z.literal(true)), + })), + spells: z.array(z.string()), + equipment: z.array(z.string()), +}); export const CharacterValidation = z.object({ id: z.number(), name: z.string(), @@ -216,18 +240,7 @@ export const CharacterValidation = z.object({ leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()), abilities: z.record(z.enum(ABILITIES), z.number().optional()), choices: z.record(z.string(), z.array(z.number())), - variables: z.object({ - health: z.number(), - mana: z.number(), - exhaustion: z.number(), - - sickness: z.array(z.object({ - id: z.string(), - state: z.number().min(1).max(7).or(z.literal(true)), - })), - spells: z.array(z.string()), - equipment: z.array(z.string()), - }), + variables: CharacterVariablesValidation, owner: z.number(), username: z.string().optional(), visibility: z.enum(["public", "private"]), @@ -241,6 +254,7 @@ export class CharacterCompiler protected _character!: Character; protected _result!: CompiledCharacter; protected _buffer: Record = {}; + private _variableDirty: boolean = false; constructor(character: Character) { @@ -299,6 +313,20 @@ export class CharacterCompiler { this._character.variables[prop] = value; this._result.variables[prop] = value; + this._variableDirty = true; + } + saveVariables() + { + if(this._variableDirty) + { + this._variableDirty = false; + useRequestFetch()(`/api/character/${this.character.id}/variables`, { + method: 'POST', + body: this._character.variables, + }).then(() => {}).catch(() => { + Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true }); + }) + } } protected add(feature?: string) { @@ -1142,10 +1170,13 @@ class AspectPicker extends BuilderTab export class CharacterSheet { + user: ComputedRef; character?: CharacterCompiler; container: HTMLElement = div(); + tabs?: HTMLDivElement & { refresh: () => void }; constructor(id: string, user: ComputedRef) { + this.user = user; const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]); this.container.replaceChildren(load); useRequestFetch()(`/api/character/${id}`).then(character => { @@ -1159,9 +1190,16 @@ export class CharacterSheet this.render(); } else - { - //ERROR - } + throw new Error(); + }).catch(() => { + this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [ + span('text-2xl font-bold tracking-wider', 'Personnage introuvable'), + span(undefined, 'Ce personnage n\'existe pas ou est privé.'), + div('flex flex-row gap-4 justify-center items-center', [ + button(text('Personnages publics'), () => useRouter().push({ name: 'character-list' }), 'px-2 py-1'), + button(text('Créer un personange'), () => useRouter().push({ name: 'character-id-edit', params: { id: 'new' } }), 'px-2 py-1') + ]) + ])) }); } render() @@ -1170,7 +1208,22 @@ export class CharacterSheet return; const character = this.character.compiled; - console.log(character); + + this.tabs = tabgroup([ + { id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) }, + + { id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) }, + + { id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) }, + + { id: 'inventory', title: [ text('Inventaire') ], content: () => [ + + ] }, + + { id: 'notes', title: [ text('Notes') ], content: () => [ + + ] }, + ], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } }); this.container.replaceChildren(div('flex flex-col justify-center gap-1', [ div("flex flex-row gap-4 justify-between", [ div(), @@ -1215,10 +1268,9 @@ export class CharacterSheet ]), div("self-center", [ - /* user && user.id === character.owner ? - button(icon("radix-icons:pencil-2"), () => { - }, "icon") - : div() */ + this.user.value && this.user.value.id === character.owner ? + button(icon("radix-icons:pencil-2"), () => useRouter().push({ name: 'character-id-edit', params: { id: this.character?.character.id } }), "p-1") + : div() ]) ]), @@ -1299,7 +1351,7 @@ export class CharacterSheet div("flex flex-col gap-4 py-1 w-80", [ div("flex flex-col py-1 gap-4", [ div("flex flex-row items-center justify-center gap-4", [ - div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]), + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") ]), @@ -1314,107 +1366,97 @@ export class CharacterSheet div("flex flex-row items-center justify-center gap-4", [ - div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]), + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") ]), character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ - character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères' }) : undefined, - character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet' }) : undefined, - character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles' }) : undefined, - character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes' }) : undefined, - character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées' }) : undefined, - character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes' }) : undefined, - character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains' }) : undefined, - character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables' }) : undefined, - character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles' }) : undefined, - character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues' }) : undefined, - character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers' }) : undefined, - character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', size: 'small' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', size: 'small' }) : undefined, + character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', size: 'small' }) : undefined, + character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', size: 'small' }) : undefined, + character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', size: 'small' }) : undefined, + character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', size: 'small' }) : undefined, + character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', size: 'small' }) : undefined, + character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', size: 'small' }) : undefined, + character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', size: 'small' }) : undefined, + character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', size: 'small' }) : undefined, + character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', size: 'small' }) : undefined, + character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', size: 'small' }) : undefined, ]) : undefined, character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ - character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères' }) : undefined, - character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures' }) : undefined, - character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes' }) : undefined, + character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', size: 'small' }) : undefined, + character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', size: 'small' }) : undefined, + character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', size: 'small' }) : undefined, ]) : undefined, div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ - div('flex flex-row items-center gap-2', [ text('Précision'), dom('span', { text: character.spellranks.precision.toString(), class: 'font-bold' }) ]), - div('flex flex-row items-center gap-2', [ text('Savoir'), dom('span', { text: character.spellranks.knowledge.toString(), class: 'font-bold' }) ]), - div('flex flex-row items-center gap-2', [ text('Instinct'), dom('span', { text: character.spellranks.instinct.toString(), class: 'font-bold' }) ]), - div('flex flex-row items-center gap-2', [ text('Oeuvres'), dom('span', { text: character.spellranks.arts.toString(), class: 'font-bold' }) ]), + div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', character.spellranks.precision.toString()) ]), + div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', character.spellranks.knowledge.toString()) ]), + div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', character.spellranks.instinct.toString()) ]), + div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', character.spellranks.arts.toString()) ]), ]) ]) ]), div('border-l border-light-35 dark:border-dark-35'), - tabgroup([ - { id: 'actions', title: [ text('Actions') ], content: () => [ - div('flex flex-col gap-8', [ - div('flex flex-col gap-2', [ - div("flex flex-row items-center justify-center gap-4", [ - div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]), - div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), - div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), - ]), - - div('flex flex-col gap-2', [ - div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), - ...(character.lists.action?.map(e => div('flex flex-col gap-1', [ - //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]), - markdown(getText(e), undefined, { tags: { a: preview } }), - ])) ?? []) - ]), - ]), - div('flex flex-col gap-2', [ - div("flex flex-row items-center justify-center gap-4", [ - div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]), - div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), - div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), - ]), - - div('flex flex-col gap-2', [ - div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), - ...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [ - //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]), - markdown(getText(e), undefined, { tags: { a: preview } }), - ])) ?? []) - ]), - ]), - div('flex flex-col gap-2', [ - div("flex flex-row items-center justify-center gap-4", [ - div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]), - div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), - ]), - - div('flex flex-col gap-2', [ - div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), - ...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [ - //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]), - markdown(getText(e), undefined, { tags: { a: preview } }), - ])) ?? []) - ]), - ]), - ]), - ] }, - - { id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) }, - - { id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) }, - - { id: 'inventory', title: [ text('Inventaire') ], content: () => [ - - ] }, - - { id: 'notes', title: [ text('Notes') ], content: () => [ - - ] }, - ], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } }), + this.tabs, ]) ])); } + actionsTab(character: CompiledCharacter) + { + return [ + div('flex flex-col gap-8', [ + div('flex flex-col gap-2', [ + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), + div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), + ]), + + div('flex flex-col gap-2', [ + div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), + ...(character.lists.action?.map(e => div('flex flex-col gap-1', [ + //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]), + markdown(getText(e), undefined, { tags: { a: preview } }), + ])) ?? []) + ]), + ]), + div('flex flex-col gap-2', [ + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), + div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]), + ]), + + div('flex flex-col gap-2', [ + div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), + ...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [ + //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]), + markdown(getText(e), undefined, { tags: { a: preview } }), + ])) ?? []) + ]), + ]), + div('flex flex-col gap-2', [ + div("flex flex-row items-center justify-center gap-4", [ + div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]), + div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), + ]), + + div('flex flex-col gap-2', [ + div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), + ...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [ + //div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]), + markdown(getText(e), undefined, { tags: { a: preview } }), + ])) ?? []) + ]), + ]), + ]), + ] + } abilitiesTab(character: CompiledCharacter) { return [ @@ -1428,23 +1470,45 @@ export class CharacterSheet } spellTab(character: CompiledCharacter) { + let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element'; + + const sort = (spells: Array<{ id: string, spell?: SpellConfig, source: string }>) => { + spells = spells.filter(e => !!e.spell); + switch(sortPreference) + { + case 'rank': return spells.sort((a, b) => a.spell!.rank - b.spell!.rank || SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!)); + case 'type': return spells.sort((a, b) => a.spell!.type.localeCompare(b.spell!.type) || a.spell!.rank - b.spell!.rank); + case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!) || a.spell!.rank - b.spell!.rank); + default: return spells; + } + }; + const spells = sort([...(character.lists.spells ?? []).map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'feature' })), ...character.variables.spells.map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'player' }))]).map(e => ({...e, dom: + e.spell ? div('flex flex-col gap-2', [ + div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${e.spell.cost ?? 0} mana` }) ]), + div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [ + div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]), + div('flex flex-row gap-2', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ]) + ]), + div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.effect) ]), + ]) : undefined })); return [ div('flex flex-col gap-2', [ div('flex flex-row justify-between items-center', [ div('flex flex-row gap-2 items-center', [ dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }), - buttongroup([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: 'rank', class: { option: 'px-2 py-1 text-sm' } }), + buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; this.tabs?.refresh(); } }), + ]), + div('flex flex-row gap-2 items-center', [ + dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }), + button(text('Modifier'), () => this.spellPanel(character, spells), 'py-1 px-4'), ]) - ]) + ]), + div('flex flex-col gap-2', spells.map(e => e.dom)) ]) ] } - spellPanel() + spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>) { - if(!this.character) - return; - - const character = this.character.compiled; const availableSpells = Object.values(config.spells).filter(spell => { if (spell.rank === 4) return false; if (character.spellranks[spell.type] < spell.rank) return false; @@ -1465,17 +1529,17 @@ export class CharacterSheet const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => { if(state === 'choosen') { - //this.character.variable('spells', character.variables.spells.filter(e => e !== spell.id)); //TO REWORK + this.character!.variable('spells', character.variables.spells.filter(e => e !== spell.id)); state = 'empty'; } else if(state === 'empty') { - //this.character.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK + this.character!.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK state = 'choosen'; } - //character = compiler.compiled; //TO REWORK toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'; textAmount.textContent = character.variables.spells.length.toString(); + this.tabs?.refresh(); }, "px-2 py-1 text-sm font-normal"); toggleButton.disabled = state === 'given'; return foldable(() => [ @@ -1507,7 +1571,7 @@ export class CharacterSheet ]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } }); })) ]); - const blocker = fullblocker([ container ], { closeWhenOutside: true }); + const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() }); setTimeout(() => container.setAttribute('data-state', 'active'), 1); } } \ No newline at end of file diff --git a/shared/components.util.ts b/shared/components.util.ts index 4c71b43..62368cf 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -35,9 +35,16 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise } export function button(content: Node, onClick?: () => void, cls?: Class) { - const btn = dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none + /* + text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 - disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: () => disabled || (onClick && onClick()) } }, [ content ]); + disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50 + */ + const btn = dom('button', { class: [`inline-flex justify-center items-center outline-none leading-none transition-[box-shadow] + text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40 + hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50 + focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50 + disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick()) } }, [ content ]); let disabled = false; Object.defineProperty(btn, 'disabled', { get: () => disabled, @@ -48,7 +55,7 @@ export function button(content: Node, onClick?: () => void, cls?: Class) }) return btn; } -export function buttongroup(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean }) +export function buttongroup(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean | void }) { let currentValue = settings?.value; const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 dark:hover:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-none @@ -60,7 +67,7 @@ export function buttongroup(options: Array<{ text: string, value: elements.forEach(e => e.toggleAttribute('data-selected', false)); this.toggleAttribute('data-selected', true); - if(!settings?.onChange || settings?.onChange(e.value)) + if(!settings?.onChange || settings?.onChange(e.value) !== false) { currentValue = e.value; } @@ -454,25 +461,38 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo }, [ 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; } -export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } }) +export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } }): HTMLDivElement & { refresh: () => void } { - const focus = settings?.focused ?? tabs[0]?.id; + let focus = settings?.focused ?? tabs[0]?.id; const titles = tabs.map((e, i) => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() { if(this.hasAttribute('data-focus')) return; titles.forEach(e => e.toggleAttribute('data-focus', false)); this.toggleAttribute('data-focus', true); + focus = e.id; const _content = typeof e.content === 'function' ? e.content() : e.content; //@ts-expect-error content.replaceChildren(..._content); }}}, e.title)); const _content = tabs.find(e => e.id === focus)?.content; const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content); - return div(['flex flex-col', settings?.class?.container], [ + + const container = div(['flex flex-col', settings?.class?.container], [ div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles), content ]); + Object.defineProperty(container, 'refresh', { + writable: false, + configurable: false, + enumerable: false, + value: () => { + const _content = tabs.find(e => e.id === focus)?.content; + //@ts-expect-error + _content && content.replaceChildren(...(typeof _content === 'function' ? _content() : _content)) + } + }) + return container as HTMLDivElement & { refresh: () => void }; } export interface ToastConfig diff --git a/shared/dom.util.ts b/shared/dom.util.ts index 62e914f..a291cfd 100644 --- a/shared/dom.util.ts +++ b/shared/dom.util.ts @@ -56,9 +56,9 @@ export function div(cls?: Class, children?: NodeChildren): HTMLDivElement { return dom("div", { class: cls }, children); } -export function span(cls?: Class, children?: NodeChildren): HTMLSpanElement +export function span(cls?: Class, text?: string): HTMLSpanElement { - return dom("span", { class: cls }, children); + return dom("span", { class: cls, text: text }); } export function svg(tag: K, properties?: NodeProperties, children?: Omit): SVGElementTagNameMap[K] { @@ -138,7 +138,7 @@ export function icon(name: string, properties?: IconProperties): HTMLElement properties?.mode && el.setAttribute('mode', properties?.mode.toString()); properties?.inline && el.toggleAttribute('inline', properties?.inline); - properties?.noobserver && el.toggleAttribute('noobserver', properties?.noobserver); + el.toggleAttribute('noobserver', properties?.noobserver ?? true); properties?.width && el.setAttribute('width', properties?.width.toString()); properties?.height && el.setAttribute('height', properties?.height.toString()); properties?.flip && el.setAttribute('flip', properties?.flip.toString()); diff --git a/shared/floating.util.ts b/shared/floating.util.ts index 6f0981c..0c50cdd 100644 --- a/shared/floating.util.ts +++ b/shared/floating.util.ts @@ -30,6 +30,7 @@ export interface ModalProperties { priority?: boolean; closeWhenOutside?: boolean; + onClose?: () => boolean | void; } let teleport: HTMLDivElement; @@ -161,6 +162,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H function link(element: HTMLElement) { Object.entries({ 'mouseenter': show, + 'mousemove': show, 'mouseleave': hide, 'focus': show, 'blur': hide, @@ -296,14 +298,13 @@ export function tooltip(container: HTMLElement, txt: string | Text, placement: F export function fullblocker(content: NodeChildren, properties?: ModalProperties) { - const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } }); + const close = () => (!properties?.onClose || properties.onClose() !== false) && _modal.remove(); + const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (close) : undefined } }); const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]); teleport.appendChild(_modal); - return { - close: () => _modal.remove(), - } + return { close }; } export function modal(content: NodeChildren, properties?: ModalProperties) { diff --git a/shared/proses.ts b/shared/proses.ts index 7e8914e..72efc62 100644 --- a/shared/proses.ts +++ b/shared/proses.ts @@ -1,4 +1,4 @@ -import { dom, icon, type NodeChildren, type Node, div } from "#shared/dom.util"; +import { dom, icon, type NodeChildren, type Node, div, type Class } from "#shared/dom.util"; import { parseURL } from 'ufo'; import render from "#shared/markdown.util"; import { popper } from "#shared/floating.util"; @@ -64,7 +64,7 @@ export const a: Prose = { } } export const preview: Prose = { - custom(properties, children) { + custom(properties: { href: string, class?: Class, size?: 'small' | 'large' }, children) { const href = properties.href as string; const { hash, pathname } = parseURL(href); const router = useRouter(); @@ -76,40 +76,36 @@ export const preview: Prose = { overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined ]); - - if(!!overview) - { - const magicKeys = useMagicKeys(); - popper(el, { - arrow: true, - delay: 150, - offset: 12, - cover: "height", - placement: 'bottom-start', - class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full z-[45]', - content: () => { - return [async('large', Content.getContent(overview.id).then((_content) => { - if(_content?.type === 'markdown') - { - return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); - } - if(_content?.type === 'canvas') - { - const canvas = new Canvas((_content as LocalContent<'canvas'>).content); - queueMicrotask(() => canvas.mount()); - return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]); - } - return div(''); - }))]; - }, - onShow() { - if(!magicKeys.current.has('control') || magicKeys.current.has('meta')) - return false; - }, - }); - } - - return el; + const magicKeys = useMagicKeys(); + return overview ? popper(el, { + arrow: true, + delay: 150, + offset: 12, + cover: "height", + placement: 'bottom-start', + class: ['data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 w-full z-[45]', + { 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]': !properties?.size || properties.size === 'large', 'max-w-[400px] max-h-[250px]': properties.size === 'small' } + ], + content: () => { + return [async('large', Content.getContent(overview.id).then((_content) => { + if(_content?.type === 'markdown') + { + return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); + } + if(_content?.type === 'canvas') + { + const canvas = new Canvas((_content as LocalContent<'canvas'>).content); + queueMicrotask(() => canvas.mount()); + return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]); + } + return div(''); + }))]; + }, + onShow() { + if(!magicKeys.current.has('control') || magicKeys.current.has('meta')) + return false; + }, + }) : el; } } export const callout: Prose = { diff --git a/types/character.d.ts b/types/character.d.ts index e983412..26ecba3 100644 --- a/types/character.d.ts +++ b/types/character.d.ts @@ -67,7 +67,7 @@ export type CharacterConfig = { features: Record; enchantments: Record; //TODO items: Record; - lists: Record }>; + lists: Record, values: Record }>; texts: Record; }; export type EnchantementConfig = { From eb0c33deae8172701c61c03dc44ada131b95c5a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Tue, 30 Sep 2025 18:03:38 +0200 Subject: [PATCH 4/4] New ability display, sereval Character compile and creation fixes --- db.sqlite | Bin 761856 -> 761856 bytes db.sqlite-shm | Bin 32768 -> 32768 bytes db.sqlite-wal | Bin 0 -> 74192 bytes pages/character/list.client.vue | 24 +++++++++++++----- server/api/character.get.ts | 2 +- server/api/character.post.ts | 6 ++--- shared/character.util.ts | 43 +++++++++++++++++--------------- 7 files changed, 45 insertions(+), 30 deletions(-) diff --git a/db.sqlite b/db.sqlite index e7966d7a1dc347b7e88df2b2ac9023a7a01a40c3..7c8cd1590cd0a53a5bd15cd374d0809d3a2be211 100644 GIT binary patch delta 8890 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 I_s;bH0?$Ht4FCWD diff --git a/db.sqlite-shm b/db.sqlite-shm index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..97e287c876c674f8fa70139c428075abaaca2e97 100644 GIT binary patch delta 255 zcmZo@U}|V!s+V}A%K!pGK+MR%AONCw0dc61r25shOP|TxRembdE7|HHow-(iM*I;{ z)dP(J1CY7@kpNV9Vm+rT8<5Qb#1zpR8`T2^85x)vSQ*$EI2pJZcp3N^1UELmW));& XU;&!V!NA49!@$QNz#z1-@xM9%OeQe9 delta 80 zcmZo@U}|V!;+1%$%K!t66E8}OTChv7nNGgVi7p`mlYpuI4+Il)WHvq!3EcP~MGpY5 C*%ZY9 diff --git a/db.sqlite-wal b/db.sqlite-wal index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6e66e0fb157e58db53ad88d2e3f970457f167ad3 100644 GIT binary patch literal 74192 zcmeI*Ux*b|9Ki9rc3r%<-diU4C(1IyTK35NIWuPl(MSo!5GoLqeAt;eGdm_h51I%; zWfJs<49!Y2Uo^u)C^Cu=v0fy62t<3x{!o&TSl0GnR9L@jx8NdO7M5Li?R#a{<#J}` z%;(&hhvCbvd8~U=MSVM?Qn|L$e9d0Caqy9O>)t!sf0w5ZEm|Ga;G=KUu> zfAMWSr7^2=P5p4Cey)DFalC%`KmMKT8Eo#A7Xk<%fB*srAbJ%-U3GFUwS7jVl|+ zo>N4bo4R4ScU7;9ez;amwTudEp+X(IN+(645+@9miE=mcLz9}^lzW!$lhKQ0->w(A zzN1x8#Io-&H!8JZpgf&Lu61H3bWFMHosBYjTCJMMbmPQmB|{RWQ`s&K+)#e5ma9XvW%&G%Vl-qz`81pz1Wd;aQs-82S2X# zO>d4)izrOxkSfqcEGD_Eyd&$Bn;?n{FDfE8DNod=$Y)wiQY+rtmE%XQm_#y(v`9jg z7m*3%RITgB<(pq`YmJu0bxb0=Z7!%z964Vb1j@!H&U6;)%(dnIi@(Td5sXr+Bb)om z3kz96*_}!P8z~tX=_GNo*aYPp`TH_j1m(kH{ag;6Tsl=Pn-Dv)^mZjUSR9lIS03`+_aZoU_351b4CCG1Q0*~0R#|00D++i&n=lFi1Q0*~0R#|0009ILKmdU;Ezr3aXg(Kc>jhTX z$&+X8{?yV7jOkTnatI)R00IagfB*srATTTedI5R?dVy9ia9S>jnP!ZO+bLS9hg%92M8d500IagfB*srATV|X=mqEn=mlE6K%ZQvZN0$Up7Sq1`}0%$eu1%j17l_g zAb(0ne?)(g0=Z0+_IZs+e23|*L!2q1s}0tg_000Iag zFzy8C1?UCn1zNp;lFPTP7wBJmdvDiQXP5K)1;*WtjR_-w00IagfB*srAb +import characterConfig from '#shared/character-config.json'; +import type { CharacterConfig } from '~/types/character'; const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } }); +const config = characterConfig as CharacterConfig;