Persistant item/spell panel to avoid filing the reactive tracker.

This commit is contained in:
Clément Pons 2025-12-16 18:07:40 +01:00
parent 78a101b79d
commit e9ffdd58a5
3 changed files with 67 additions and 29 deletions

BIN
db.sqlite

Binary file not shown.

View File

@ -3,7 +3,7 @@ import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json'; import characterConfig from '#shared/character-config.json';
import proses, { preview } from "#shared/proses"; 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 { 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 { followermenu, fullblocker, tooltip } from "#shared/floating.util";
import { clamp, deepEquals } from "#shared/general.util"; import { clamp, deepEquals } from "#shared/general.util";
import markdown from "#shared/markdown.util"; import markdown from "#shared/markdown.util";
@ -11,7 +11,7 @@ import { getText } from "#shared/i18n";
import type { User } from "~/types/auth"; import type { User } from "~/types/auth";
import { MarkdownEditor } from "#shared/editor.util"; import { MarkdownEditor } from "#shared/editor.util";
import { Socket } from "#shared/websocket.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; const config = characterConfig as CharacterConfig;
@ -286,7 +286,6 @@ export class CharacterCompiler
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] }, 'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/psyche': { 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(() => {}); private _variableDebounce: NodeJS.Timeout = setTimeout(() => {});
constructor(character: Character) constructor(character: Character)
@ -307,11 +306,6 @@ export class CharacterCompiler
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] }, 'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/psyche': { 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) if(value.people !== undefined)
{ {
@ -350,7 +344,7 @@ export class CharacterCompiler
get armor() get armor()
{ {
const armors = this._character.variables.items.filter(e => e.equipped && config.items[e.id]?.category === '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 parse(text: string): string
@ -362,12 +356,15 @@ export class CharacterCompiler
} }
saveVariables() saveVariables()
{ {
clearTimeout(this._variableDebounce);
this._variableDebounce = setTimeout(() => {
useRequestFetch()(`/api/character/${this.character.id}/variables`, { useRequestFetch()(`/api/character/${this.character.id}/variables`, {
method: 'POST', method: 'POST',
body: raw(this._character.variables), body: raw(this._character.variables),
}).then(() => {}).catch(() => { }).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true }); Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
}) })
}, 2000);
} }
saveNotes() saveNotes()
{ {
@ -1284,6 +1281,17 @@ const subnameFactory = (item: ItemConfig, state?: ItemState): string[] => {
return result; 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 export class CharacterSheet
{ {
private user: ComputedRef<User | null>; private user: ComputedRef<User | null>;
@ -1661,6 +1669,8 @@ export class CharacterSheet
} }
}; };
const panel = this.spellPanel(character);
return [ return [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-row justify-between items-center', [ div('flex flex-row justify-between items-center', [
@ -1670,7 +1680,7 @@ export class CharacterSheet
]), ]),
div('flex flex-row gap-2 items-center', [ 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' : '') }), 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 => { div('flex flex-col gap-2', { render: e => {
@ -1736,13 +1746,22 @@ export class CharacterSheet
const idx = spells.findIndex(e => e === spell.id); const idx = spells.findIndex(e => e === spell.id);
if(idx !== -1) spells.splice(idx, 1); if(idx !== -1) spells.splice(idx, 1);
else spells.push(spell.id); else spells.push(spell.id);
this.character?.saveVariables();
}, "px-2 py-1 text-sm font-normal"), }, "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' } }) ]) ], { 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() }); const blocker = fullblocker([ container ], { closeWhenOutside: true, open: false });
return { show: () => {
setTimeout(() => container.setAttribute('data-state', 'active'), 1); setTimeout(() => container.setAttribute('data-state', 'active'), 1);
blocker.open();
}, hide: () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}};
} }
itemsTab(character: CompiledCharacter) 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 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 weight = () => items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0);
const panel = this.itemsPanel(character);
return [ return [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-row justify-end items-center gap-8', [ 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': 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}` }), 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 => { 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]; const item = config.items[e.id];
@ -1776,19 +1797,25 @@ export class CharacterSheet
items[idx]!.amount--; items[idx]!.amount--;
if(items[idx]!.amount <= 0) items.splice(idx, 1); if(items[idx]!.amount <= 0) items.splice(idx, 1);
this.character?.saveVariables();
}, 'p-1'), }, 'p-1'),
button(icon('radix-icons:plus', { width: 12, height: 12 }), () => { button(icon('radix-icons:plus', { width: 12, height: 12 }), () => {
const idx = items.findIndex(_e => _e === e); const idx = items.findIndex(_e => _e === e);
if(idx === -1) return; 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 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'), }, 'p-1'),
]) ], [div('flex flex-row justify-between', [ ]) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [ div('flex flex-row items-center gap-4', [
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => { item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
e.equipped = v; e.equipped = v;
this.character?.saveVariables();
}, class: { container: '!w-5 !h-5' } }) : undefined, }, 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))) ]), 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}` : '-') ]), 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 }), () => { button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
const list = this.character!.character.variables.items; 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 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'), }, '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' } }) ])], { 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() }); const blocker = fullblocker([ container ], { closeWhenOutside: true, open: false });
return { show: () => {
setTimeout(() => container.setAttribute('data-state', 'active'), 1); setTimeout(() => container.setAttribute('data-state', 'active'), 1);
blocker.open();
}, hide: () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}};
} }
} }

View File

@ -35,6 +35,7 @@ export interface PopperProperties extends FloatingProperties
export interface ModalProperties export interface ModalProperties
{ {
priority?: boolean; priority?: boolean;
open?: boolean;
class?: { blocker?: Class, popup?: Class }, class?: { blocker?: Class, popup?: Class },
closeWhenOutside?: boolean; closeWhenOutside?: boolean;
onClose?: () => boolean | void; onClose?: () => boolean | void;
@ -390,15 +391,16 @@ export function tooltip(container: RedrawableHTML, txt: string | Text, placement
export function fullblocker(content: NodeChildren, properties?: ModalProperties) export function fullblocker(content: NodeChildren, properties?: ModalProperties)
{ {
if(!content) 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 _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]); 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 } }) export function modal(content: NodeChildren, properties?: ModalProperties & { class?: { container?: Class } })
{ {