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 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<User | null>;
@ -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');
}};
}
}

View File

@ -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 } })
{