diff --git a/db.sqlite b/db.sqlite index ddcf3e2..61ed848 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-shm b/db.sqlite-shm index 075064a..95530a0 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index 421e9f4..54e6c1b 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/pages/character/[id]/index.client.vue b/pages/character/[id]/index.client.vue index 099613c..8a712ad 100644 --- a/pages/character/[id]/index.client.vue +++ b/pages/character/[id]/index.client.vue @@ -3,11 +3,15 @@ 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 type { SpellConfig } from '~/types/character'; +import type { CompiledCharacter, SpellConfig } from '~/types/character'; import type { CharacterConfig } from '~/types/character'; -import { abilityTexts, CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '~/shared/character.util'; -import { getText } from '~/shared/i18n'; -import { fakeA } from '~/shared/proses'; +import { abilityTexts, CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '#shared/character.util'; +import { getText } from '#shared/i18n'; +import { fakeA } 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; @@ -16,7 +20,7 @@ const { user } = useUserSession(); const { data, status, error } = await useFetch(`/api/character/${id}`); const compiler = new CharacterCompiler(data.value ?? defaultCharacter); -const character = ref(compiler.compiled); +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 @@ -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); }