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] 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