From 423df7bc42b99337407f303de6f67ca2edd187b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Mon, 1 Sep 2025 17:53:07 +0200 Subject: [PATCH] Add spell picker in the character sheet --- db.sqlite | Bin 761856 -> 761856 bytes db.sqlite-shm | Bin 32768 -> 32768 bytes db.sqlite-wal | Bin 8272 -> 32992 bytes pages/character/[id]/index.client.vue | 82 +++++++++++++++++++++++--- shared/character-config.json | 2 +- shared/character.util.ts | 20 ++----- shared/components.util.ts | 13 +++- shared/i18n.ts | 4 +- shared/markdown.util.ts | 2 +- 9 files changed, 95 insertions(+), 28 deletions(-) diff --git a/db.sqlite b/db.sqlite index ddcf3e2f4d224f38380c6adfc3c25cabc5b7ac63..61ed8486f101ef7f1642ef0fc5177d14d92be4bf 100644 GIT binary patch delta 65 zcmZoTpx1CfZ^NrNrb7&q-^6J%Z$03+St|aq0*KMn(AL1%*1*)(z}(ir($>J**1*=* Tz~0ut(bmA(*1)x_fm;Cpqi`As delta 65 zcmV-H0KWf#z%GEmF0kre0=!C->|QGavC00k8eiie1GKzKf`EpA0fvAAhJXWxfCPqs X1%`kIhJXi#fCz?w35I|Qrhp3|)h!zm diff --git a/db.sqlite-shm b/db.sqlite-shm index 075064a833a324519e3b68f7dfcf0b0dfc98cd1d..95530a0c9878c07e09c5323094325064e1bbb687 100644 GIT binary patch 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 delta 156 zcmZo@U}|V!s+V}A%K!pQK+MR%AONCwf$)t5_S51%{k)fBkQyrEC12E5lQJ(|R*+Qn mK%>9_WbS_?02Q8CFSv1qPvFD@5{!%+H~wa2WZJlqUjqP1d@&9H diff --git a/db.sqlite-wal b/db.sqlite-wal index 421e9f4999b34e4c349094b632c2155b48fba7b5..54e6c1bbe4a18e0204f55a2d8dbefa5476a4ff66 100644 GIT binary patch delta 286 zcmccM@Su^&!n~fXi9z>~1OtNr0|@9GboIWSg=ZJk%?7#g$AvPf11ry)waP83M zlg$4%T8Q&AZ#_^uIf(zA08Da1@6TuPF014>2MGM-mj=qB>*lFQTYFWe-wmq!F^cYQ z^Ip52o&EVdO!s4S-A8=YKWOaV9|zU_3`KW}OR|jj**>`LXXv_Jw=RBrm%s8ZME5~C V6y2h4baE4AZ7zZBX5M;G4ghb&bm#y8 delta 100 zcmaFR$aKNM!n~fXi9z>~1OtNr0|?y9F-Q%S@shU^&;Kasnfw|k#0JE>VB$uJ`TLwX oLuPKY5a(y!c=F%mApUm(FiHCbH+KE}6gO>ifWTjV<}HU900$K!!T(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 @@ -29,9 +33,71 @@ 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 manageSpell() -{ +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 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); } @@ -162,7 +228,7 @@ function manageSpell()
-
{{ character.variables.spells.length }} / {{ character.spellslots }} sorts maitrisés
+
{{ character.variables.spells.length }} / {{ character.spellslots }} sorts maitrisés
diff --git a/shared/character-config.json b/shared/character-config.json index 58c1e7a..8b8c8a8 100644 --- a/shared/character-config.json +++ b/shared/character-config.json @@ -2499,7 +2499,7 @@ { "id": "9jq3pkj7sgfgq6q4ovwoanig6ha8g2ic", "name": "Dévastation élémentaire", - "rank": 1, + "rank": 4, "type": "precision", "cost": 8, "speed": "action", diff --git a/shared/character.util.ts b/shared/character.util.ts index b656006..7b64b7e 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -1,4 +1,4 @@ -import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel } from "~/types/character"; +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"; @@ -293,6 +293,11 @@ export class CharacterCompiler return substring; }) } + variable(prop: T, value: CharacterVariables[T]) + { + this._character.variables[prop] = value; + this._result.variables[prop] = value; + } protected add(feature?: string) { if(!feature) @@ -1125,22 +1130,9 @@ class AspectPicker extends BuilderTab } static override validate(builder: CharacterBuilder): boolean { - /* const physic = Object.values(builder.character.training['strength']).length + Object.values(builder.character.training['dexterity']).length + Object.values(builder.character.training['constitution']).length; - const mental = Object.values(builder.character.training['intelligence']).length + Object.values(builder.character.training['curiosity']).length; - const personality = Object.values(builder.character.training['charisma']).length + Object.values(builder.character.training['psyche']).length; */ - if(builder.character.aspect === undefined) return false; - /* const aspect = config.aspects[builder.character.aspect]! - - if(physic > aspect.physic.max || physic < aspect.physic.min) - return false; - if(mental > aspect.mental.max || mental < aspect.mental.min) - return false; - if(personality > aspect.personality.max || personality < aspect.personality.min) - return false; */ - return true; } } \ No newline at end of file diff --git a/shared/components.util.ts b/shared/components.util.ts index 9f336db..84565f3 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -35,9 +35,18 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise } export function button(content: Node, onClick?: () => void, cls?: Class) { - return 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 + 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 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: onClick } }, [ content ]); + 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 ]); + let disabled = false; + Object.defineProperty(btn, 'disabled', { + get: () => disabled, + set: (v) => { + disabled = !!v; + btn.toggleAttribute('disabled', disabled); + } + }) + return btn; } export type Option = { text: string, render?: () => HTMLElement, value: T | Option[] } | undefined; type StoredOption = { item: Option, dom: HTMLElement, container?: HTMLElement, children?: Array> }; diff --git a/shared/i18n.ts b/shared/i18n.ts index 847c279..5398707 100644 --- a/shared/i18n.ts +++ b/shared/i18n.ts @@ -3,7 +3,7 @@ import characterConfig from '#shared/character-config.json'; const config = characterConfig as CharacterConfig; -export function getText(id?: i18nID, lang?: string) +export function getText(id?: i18nID, lang?: string): string { - return id ? (config.texts.hasOwnProperty(id) ? config.texts[id][lang ?? "default"] : '') : undefined; + return id ? (config.texts.hasOwnProperty(id) ? config.texts[id][lang ?? "default"] : '') : ''; } \ No newline at end of file diff --git a/shared/markdown.util.ts b/shared/markdown.util.ts index d575c32..a83a38c 100644 --- a/shared/markdown.util.ts +++ b/shared/markdown.util.ts @@ -29,7 +29,7 @@ function renderContent(node: RootContent, proses: Record): Node { const children = node.children.map(e => renderContent(e, proses)), properties = { ...node.properties, class: node.properties.className as string | string[] }; if(node.tagName in proses) - return prose(node.tagName, proses[node.tagName], children, properties); + return prose(node.tagName, proses[node.tagName] ?? { class: '' }, children, properties); else return dom(node.tagName as keyof HTMLElementTagNameMap, properties, children); }