diff --git a/db.sqlite b/db.sqlite index e600ce1..2c5702e 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/shared/character.util.ts b/shared/character.util.ts index 157bc48..5a52fbc 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -3,7 +3,7 @@ import { z } from "zod/v4"; import characterConfig from '#shared/character-config.json'; import proses, { preview } from "#shared/proses"; import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util"; -import { div, dom, icon, span, text, reactive, type DOMList, type RedrawableHTML } from "#shared/dom.util"; +import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom.util"; import { followermenu, fullblocker, tooltip } from "#shared/floating.util"; import { clamp, deepEquals } from "#shared/general.util"; import markdown from "#shared/markdown.util"; @@ -11,7 +11,7 @@ import { getText } from "#shared/i18n"; import type { User } from "~/types/auth"; import { MarkdownEditor } from "#shared/editor.util"; import { Socket } from "#shared/websocket.util"; -import { raw, reactive, reactivity, type Reactive } from '#shared/reactive'; +import { raw, reactive } from '#shared/reactive'; const config = characterConfig as CharacterConfig; @@ -286,7 +286,6 @@ export class CharacterCompiler 'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] }, 'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] }, }; - private _variableDirty: boolean = false; private _variableDebounce: NodeJS.Timeout = setTimeout(() => {}); constructor(character: Character) @@ -307,11 +306,6 @@ export class CharacterCompiler 'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] }, 'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] }, }; - reactivity(() => this.character.variables, () => { - console.log("Saving variables"); - clearTimeout(this._variableDebounce); - this._variableDebounce = setTimeout(() => this.saveVariables(), 2000); - }) if(value.people !== undefined) { @@ -350,7 +344,7 @@ export class CharacterCompiler get armor() { const armors = this._character.variables.items.filter(e => e.equipped && config.items[e.id]?.category === 'armor'); - return armors.length > 0 ? armors.map(e => ({ max: (config.items[e.id] as ArmorConfig).health, current: (config.items[e.id] as ArmorConfig).health - e.state.health })).reduce((p, v) => { p.max += v.max; p.current += v.current; return p; }, { max: 0, current: 0 }) : undefined; + return armors.length > 0 ? armors.map(e => ({ max: (config.items[e.id] as ArmorConfig).health, current: (config.items[e.id] as ArmorConfig).health - e.state })).reduce((p, v) => { p.max += v.max; p.current += v.current; return p; }, { max: 0, current: 0 }) : undefined; } parse(text: string): string @@ -362,12 +356,15 @@ export class CharacterCompiler } saveVariables() { - useRequestFetch()(`/api/character/${this.character.id}/variables`, { - method: 'POST', - body: raw(this._character.variables), - }).then(() => {}).catch(() => { - Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true }); - }) + clearTimeout(this._variableDebounce); + this._variableDebounce = setTimeout(() => { + useRequestFetch()(`/api/character/${this.character.id}/variables`, { + method: 'POST', + body: raw(this._character.variables), + }).then(() => {}).catch(() => { + Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true }); + }) + }, 2000); } saveNotes() { @@ -1284,6 +1281,17 @@ const subnameFactory = (item: ItemConfig, state?: ItemState): string[] => { return result; } +const stateFactory = (item: ItemConfig) => { + const state = { id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: item.equippable ? false : undefined } as ItemState; + switch(item.category) + { + case 'armor': + state.state = 0; + break; + default: break; + } + return state; +} export class CharacterSheet { private user: ComputedRef; @@ -1660,6 +1668,8 @@ export class CharacterSheet default: return spells; } }; + + const panel = this.spellPanel(character); return [ div('flex flex-col gap-2', [ @@ -1670,7 +1680,7 @@ export class CharacterSheet ]), div('flex flex-row gap-2 items-center', [ dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length + (character.lists.spells?.length ?? 0) !== character.spellslots }], text: () => `${character.variables.spells.length + (character.lists.spells?.length ?? 0)}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', character.variables.spells.length + (character.lists.spells?.length ?? 0) > 1 ? 's' : '') }), - button(text('Modifier'), () => this.spellPanel(character), 'py-1 px-4'), + button(text('Modifier'), () => panel.show(), 'py-1 px-4'), ]) ]), div('flex flex-col gap-2', { render: e => { @@ -1736,13 +1746,22 @@ export class CharacterSheet const idx = spells.findIndex(e => e === spell.id); if(idx !== -1) spells.splice(idx, 1); else spells.push(spell.id); + + this.character?.saveVariables(); }, "px-2 py-1 text-sm font-normal"), ]), ]) ], { 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, onClose: () => this.character?.saveVariables() }); - setTimeout(() => container.setAttribute('data-state', 'active'), 1); + const blocker = fullblocker([ container ], { closeWhenOutside: true, open: false }); + + return { show: () => { + setTimeout(() => container.setAttribute('data-state', 'active'), 1); + blocker.open(); + }, hide: () => { + setTimeout(blocker.close, 150); + container.setAttribute('data-state', 'inactive'); + }}; } itemsTab(character: CompiledCharacter) { @@ -1750,12 +1769,14 @@ export class CharacterSheet const power = () => items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + ((config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0) * v.amount), 0); const weight = () => items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0); + const panel = this.itemsPanel(character); + return [ div('flex flex-col gap-2', [ div('flex flex-row justify-end items-center gap-8', [ dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': weight() > character.itempower }], text: () => `Poids total: ${weight()}/${character.itempower}` }), dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': power() > (character.capacity === false ? 0 : character.capacity) }], text: () => `Puissance magique: ${power()}/${character.capacity}` }), - button(text('Modifier'), () => this.itemsPanel(character), 'py-1 px-4'), + button(text('Modifier'), () => panel.show(), 'py-1 px-4'), ]), div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: character.variables.items, render: e => { const item = config.items[e.id]; @@ -1776,19 +1797,25 @@ export class CharacterSheet items[idx]!.amount--; if(items[idx]!.amount <= 0) items.splice(idx, 1); + + this.character?.saveVariables(); }, 'p-1'), button(icon('radix-icons:plus', { width: 12, height: 12 }), () => { const idx = items.findIndex(_e => _e === e); if(idx === -1) return; - if(item.equippable) items.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false }); + if(item.equippable) items.push(stateFactory(item)); else if(items.find(_e => _e === e)) items.find(_e => _e === e)!.amount++; - else items.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] }); + else items.push(stateFactory(item)); + + this.character?.saveVariables(); }, 'p-1'), ]) ], [div('flex flex-row justify-between', [ div('flex flex-row items-center gap-4', [ item.equippable ? checkbox({ defaultValue: e.equipped, change: v => { e.equipped = v; + + this.character?.saveVariables(); }, class: { container: '!w-5 !h-5' } }) : undefined, div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]), ]), @@ -1847,15 +1874,24 @@ export class CharacterSheet div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.price ? `${item.price}` : '-') ]), button(icon('radix-icons:plus', { width: 16, height: 16 }), () => { const list = this.character!.character.variables.items; - if(item.equippable) list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [], equipped: false }); + if(item.equippable) list.push(stateFactory(item)); else if(list.find(e => e.id === item.id)) list.find(e => e.id === item.id)!.amount++; - else list.push({ id: item.id, amount: 1, charges: item.charge, enchantments: [] }); + else list.push(stateFactory(item)); + + this.character?.saveVariables(); }, 'p-1 !border-solid !border-r'), ]), ])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } }) } }), ]); - const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() }); - setTimeout(() => container.setAttribute('data-state', 'active'), 1); + const blocker = fullblocker([ container ], { closeWhenOutside: true, open: false }); + + return { show: () => { + setTimeout(() => container.setAttribute('data-state', 'active'), 1); + blocker.open(); + }, hide: () => { + setTimeout(blocker.close, 150); + container.setAttribute('data-state', 'inactive'); + }}; } } \ No newline at end of file diff --git a/shared/floating.util.ts b/shared/floating.util.ts index 2b28de0..789e1af 100644 --- a/shared/floating.util.ts +++ b/shared/floating.util.ts @@ -35,6 +35,7 @@ export interface PopperProperties extends FloatingProperties export interface ModalProperties { priority?: boolean; + open?: boolean; class?: { blocker?: Class, popup?: Class }, closeWhenOutside?: boolean; onClose?: () => boolean | void; @@ -390,15 +391,16 @@ export function tooltip(container: RedrawableHTML, txt: string | Text, placement export function fullblocker(content: NodeChildren, properties?: ModalProperties) { if(!content) - return { close: () => {} }; + return { close: () => {}, open: () => {} }; - const close = () => (!properties?.onClose || properties.onClose() !== false) && _modal.remove(); + const open = () => { _modal.parentElement === null && teleport.appendChild(_modal) }; + const close = () => { _modal.parentElement !== null && (!properties?.onClose || properties.onClose() !== false) && _modal.remove() }; const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }, properties?.class?.blocker], listeners: { click: properties?.closeWhenOutside ? (close) : undefined } }); const _modal = dom('div', { class: ['fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40', properties?.class?.blocker] }, [ _modalBlocker, ...content]); - teleport.appendChild(_modal); + (properties?.open ?? true) && open(); - return { close }; + return { close, open }; } export function modal(content: NodeChildren, properties?: ModalProperties & { class?: { container?: Class } }) {