Implement Aspect tab and HP/Mana editor

This commit is contained in:
2026-01-28 21:38:10 +01:00
parent a412116b9c
commit 3081c05b55
13 changed files with 165 additions and 38 deletions

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, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components";
import { div, dom, icon, span, text, type HTMLElement } from "#shared/dom";
import { div, dom, icon, span, text } from "#shared/dom";
import { followermenu, fullblocker, tooltip } from "#shared/floating";
import { clamp } from "#shared/general";
import markdown from "#shared/markdown";
@@ -11,7 +11,7 @@ import { getText } from "#shared/i18n";
import type { User } from "~/types/auth";
import { MarkdownEditor } from "#shared/editor";
import { Socket } from "#shared/websocket";
import { raw, reactive } from '#shared/reactive';
import { raw, reactive, reactivity } from '#shared/reactive';
const config = characterConfig as CharacterConfig;
@@ -47,6 +47,7 @@ export const defaultCharacter: Character = {
sickness: [],
poisons: [],
money: 0,
transformed: false,
},
owner: -1,
@@ -259,6 +260,7 @@ export const CharacterVariablesValidation = z.object({
items: z.array(ItemStateValidation),
money: z.number(),
transformed: z.boolean(),
});
export const CharacterValidation = z.object({
id: z.number(),
@@ -323,11 +325,31 @@ export class CharacterCompiler
{
Object.entries(value.leveling).forEach(e => this.add(config.peoples[value.people!]!.options[parseInt(e[0]) as Level][e[1]]!));
MAIN_STATS.forEach(stat => {
Object.entries(value.training[stat]).forEach(option => this.add(config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]))
});
MAIN_STATS.forEach(stat => Object.entries(value.training[stat]).forEach(option => this.add(config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]])));
Object.entries(value.abilities).forEach(e => this._buffer[`abilities/${e[0]}`] = { value: 0, _dirty: true, min: -Infinity, list: [{ id: '', operation: 'add', value: e[1] }] })
Object.entries(value.abilities).forEach(e => this._buffer[`abilities/${e[0]}`] = { value: 0, _dirty: true, min: -Infinity, list: [{ id: '', operation: 'add', value: e[1] }] });
reactivity(() => value.variables.transformed, (v) => {
if(value.aspect && config.aspects[value.aspect])
{
const aspect = config.aspects[value.aspect]!;
if(v)
{
aspect.options.forEach((e) => this.apply(e));
this._buffer[`modifier/${aspect.stat}`]!.list.push({ id: 'aspect', operation: 'add', value: 1 });
this._buffer[`modifier/${aspect.stat}`]!._dirty = true;
}
else
{
aspect.options.forEach((e) => this.undo(e));
const idx = this._buffer[`modifier/${aspect.stat}`]!.list.findIndex(e => e.id === 'aspect');
idx !== -1 && this._buffer[`modifier/${aspect.stat}`]!.list.splice(idx, 1);
this._buffer[`modifier/${aspect.stat}`]!._dirty = true;
}
this.compile([`modifier/${aspect.stat}`]);
this.saveVariables();
}
})
}
}
get character(): Character
@@ -385,8 +407,8 @@ 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 as ArmorState)?.loss ?? 0) })).reduce((p, v) => { p.max += v.max; p.current += v.current; return p; }, { max: 0, current: 0 }) : undefined;
const armor = this._character.variables.items.find(e => e.equipped && config.items[e.id]?.category === 'armor');
return armor ? { max: (config.items[armor.id] as ArmorConfig).health, current: (config.items[armor.id] as ArmorConfig).health - ((armor.state as ArmorState)?.loss ?? 0), flat: (config.items[armor.id] as ArmorConfig).absorb.static + ((armor.state as ArmorState)?.absorb.flat ?? 0), percent: (config.items[armor.id] as ArmorConfig).absorb.percent + ((armor.state as ArmorState)?.absorb.percent ?? 0) } : undefined;
}
get weight()
{
@@ -1573,6 +1595,8 @@ export class CharacterSheet
const publicNotes = new MarkdownEditor();
const privateNotes = new MarkdownEditor();
const healthPanel = this.healthPanel(character);
const loadableIcon = icon('radix-icons:paper-plane', { width: 16, height: 16 });
const saveLoading = loading('small');
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.character?.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
@@ -1582,7 +1606,7 @@ export class CharacterSheet
publicNotes.content = this.character!.character.notes!.public!;
privateNotes.content = this.character!.character.notes!.private!;
const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: HTMLElement }) => {
/* const validateProperty = (v: string, property: 'health' | 'mana', obj: { edit: HTMLInputElement, readonly: HTMLElement }) => {
character.variables[property] = v.startsWith('-') ? character.variables[property] + parseInt(v.substring(1), 10) : v.startsWith('+') ? character.variables[property] - parseInt(v.substring(1), 10) : character[property] - parseInt(v, 10);
obj.edit.value = (character[property] - character.variables[property]).toString();
obj.edit.replaceWith(obj.readonly);
@@ -1608,7 +1632,7 @@ export class CharacterSheet
edit: input('text', { defaultValue: (character.mana - character.variables.mana).toString(), input: (v) => {
return v.startsWith('-') || v.startsWith('+') ? v.length === 1 || !isNaN(parseInt(v.substring(1), 10)) : v.length === 0 || !isNaN(parseInt(v, 10));
}, change: (v) => validateProperty(v, 'mana', mana), blur: () => validateProperty(mana.edit.value, 'mana', mana), class: 'font-bold px-2 w-20 text-center' }),
};
}; */
this.tabs = tabgroup([
{ id: 'actions', title: [ text('Actions') ], content: this.actionsTab(character) },
@@ -1619,7 +1643,7 @@ export class CharacterSheet
{ id: 'inventory', title: [ text('Inventaire') ], content: this.itemsTab(character) },
{ id: 'aspect', title: [ text('Aspect') ], content: () => this.aspectTab(character) },
{ id: 'aspect', title: [ span(() => ({ 'relative before:absolute before:top-0 before:-right-2 before:w-2 before:h-2 before:rounded-full before:bg-accent-blue': character.variables.transformed }), 'Aspect') ], content: () => this.aspectTab(character) },
{ id: 'effects', title: [ text('Afflictions') ], content: () => this.effectsTab(character) },
@@ -1633,7 +1657,6 @@ export class CharacterSheet
[ span('text-lg font-bold', 'Notes privés'), ], {
class: { container: 'flex flex-col gap-2 data-[active]:flex-1 py-2', content: 'h-full' }, open: false
}),
//div('flex flex-col gap-2', [ span('text-lg font-bold', 'Notes privés'), div('border border-light-35 dark:border-dark-35 bg-light20 dark:bg-dark-20 p-1 h-64', [ privateNotes.dom ]) ]),
])
] },
], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 max-w-[960px] h-full', content: 'overflow-auto h-full' }, switch: v => { this.tab = v; } });
@@ -1664,12 +1687,22 @@ export class CharacterSheet
div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("PV: "),
health.readonly,
dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: () => `${character.health - character.variables.health}`,
listeners: { click: healthPanel.show },
}),
text('/'),
text(() => character.health),
]),
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("Mana: "),
mana.readonly,
dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: () => `${character.mana - character.variables.mana}`,
listeners: { click: healthPanel.show },
}),
text('/'),
text(() => character.mana),
]),
]),
@@ -1685,31 +1718,31 @@ export class CharacterSheet
div("flex flex-row justify-center 2xl:gap-4 gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.strength}`) ]),
dom("span", { class: () => ["2xl:text-2xl text-xl font-bold", { 'text-accent-blue': character.variables.transformed && config.aspects[character.aspect.id]?.stat === 'strength' }] }, [ text(() => `+${character.modifier.strength}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Force" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.dexterity}`) ]),
dom("span", { class: () => ["2xl:text-2xl text-xl font-bold", { 'text-accent-blue': character.variables.transformed && config.aspects[character.aspect.id]?.stat === 'dexterity' }] }, [ text(() => `+${character.modifier.dexterity}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Dextérité" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.constitution}`) ]),
dom("span", { class: () => ["2xl:text-2xl text-xl font-bold", { 'text-accent-blue': character.variables.transformed && config.aspects[character.aspect.id]?.stat === 'constitution' }] }, [ text(() => `+${character.modifier.constitution}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Constitution" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.intelligence}`) ]),
dom("span", { class: () => ["2xl:text-2xl text-xl font-bold", { 'text-accent-blue': character.variables.transformed && config.aspects[character.aspect.id]?.stat === 'intelligence' }] }, [ text(() => `+${character.modifier.intelligence}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Intelligence" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.curiosity}`) ]),
dom("span", { class: () => ["2xl:text-2xl text-xl font-bold", { 'text-accent-blue': character.variables.transformed && config.aspects[character.aspect.id]?.stat === 'curiosity' }] }, [ text(() => `+${character.modifier.curiosity}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Curiosité" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.charisma}`) ]),
dom("span", { class: () => ["2xl:text-2xl text-xl font-bold", { 'text-accent-blue': character.variables.transformed && config.aspects[character.aspect.id]?.stat === 'charisma' }] }, [ text(() => `+${character.modifier.charisma}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Charisme" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold" }, [ text(() => `+${character.modifier.psyche}`) ]),
dom("span", { class: () => ["2xl:text-2xl text-xl font-bold", { 'text-accent-blue': character.variables.transformed && config.aspects[character.aspect.id]?.stat === 'psyche' }] }, [ text(() => `+${character.modifier.psyche}`) ]),
dom("span", { class: "text-sm 2xl:text-base", text: "Psyché" })
])
]),
@@ -1784,6 +1817,84 @@ export class CharacterSheet
])
]));
}
healthPanel(character: CompiledCharacter)
{
const inputs = reactive({
health: {
sum: 0,
slashing: 0,
piercing: 0,
bludgening: 0,
magic: 0,
fire: 0,
thunder: 0,
cold: 0,
open: false,
},
mana: 0,
});
const armor = this.character?.armor;
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-[480px] 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', [
div('flex flex-row gap-8 items-center', [ span('text-xl font-bold', 'Edititon de vie'), div('flex flex-row items-center gap-1', [ span('text-xl font-bold', () => (character.health - character.variables.health)), text('/'), text(() => character.health) ]) ]),
tooltip(button(icon("radix-icons:cross-1", { width: 24, height: 24 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left")
]),
foldable([
div('flex flex-col w-full gap-2 ms-2 ps-4 border-l border-light-35 dark:border-dark-35', DAMAGE_TYPES.map(e => div('flex flex-row justify-between items-center', [
span('text-lg', damageTypeTexts[e]), div('flex flew-row gap-4 justify-end', [ () => /* Res/Vul/Immun */ div('w-8'), numberpicker({ defaultValue: () => inputs.health[e], input: v => { inputs.health[e] = v; inputs.health.sum = DAMAGE_TYPES.reduce((p, v) => p + inputs.health[v], 0) }, min: 0, class: 'h-8 !m-0' }), div('w-8') ]),
])))
], [
div('flex flex-row justify-between items-center', [
span('text-lg', 'Total'), div('flex flew-row gap-4 justify-end', [
() => armor ? tooltip(button(div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:shoulder-armor', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => `${armor.current}/${armor.max} (${[armor.flat > 0 ? '-' + armor.flat : undefined, armor.percent > 0 ? armor.percent + '%' : undefined].filter(e => !!e).join('/')})`) ]), () => {
//TODO
}, 'px-2 h-8 border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:border-light-red dark:focus:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red'), 'Dégats', 'left') : undefined,
tooltip(button(icon('radix-icons:minus', { width: 16, height: 16 }), () => {
character.variables.health += inputs.health.sum;
inputs.health.sum = 0;
DAMAGE_TYPES.forEach(e => inputs.health[e] = 0);
this.character?.saveVariables();
}, 'w-8 h-8 border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:border-light-red dark:focus:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red'), 'Dégats', 'left'),
numberpicker({ defaultValue: () => inputs.health.sum, input: v => { inputs.health.sum = v }, min: 0, disabled: () => inputs.health.open, class: 'h-8 !m-0' }),
tooltip(button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
character.variables.health = Math.max(character.variables.health - inputs.health.sum, 0);
inputs.health.sum = 0;
DAMAGE_TYPES.forEach(e => inputs.health[e] = 0);
this.character?.saveVariables();
}, 'w-8 h-8 border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green focus:border-light-green dark:focus:border-dark-green focus:shadow-light-green dark:focus:shadow-dark-green'), 'Soin', 'left'),
])
])
], { class: { container: 'gap-2', title: 'ps-2' }, open: false, onFold: v => { inputs.health.open = v; if(v) { inputs.health.sum = 0; }} }),
div('flex flex-row justify-between items-center', [
div('flex flex-row gap-8 items-center', [ span('text-xl font-bold', 'Mana'), div('flex flex-row items-center gap-1', [ span('text-xl font-bold', () => (character.mana - character.variables.mana)), text('/'), text(() => character.mana) ]) ]),
div('flex flex-row gap-4 justify-end', [
tooltip(button(icon('radix-icons:minus', { width: 16, height: 16 }), () => {
character.variables.mana += inputs.mana;
inputs.mana = 0;
this.character?.saveVariables();
}, 'w-8 h-8 border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:border-light-red dark:focus:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red'), 'Dégats', 'left'),
numberpicker({ defaultValue: () => inputs.mana, input: v => { inputs.mana = v }, min: 0, class: 'h-8 !m-0' }),
tooltip(button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
character.variables.mana = Math.max(character.variables.mana - inputs.mana, 0);
inputs.mana = 0;
this.character?.saveVariables();
}, 'w-8 h-8 border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green focus:border-light-green dark:focus:border-dark-green focus:shadow-light-green dark:focus:shadow-dark-green'), 'Soin', 'left'),
])
]),
]);
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');
}};
}
actionsTab(character: CompiledCharacter)
{
return [
@@ -2222,7 +2333,17 @@ export class CharacterSheet
aspectTab(character: CompiledCharacter)
{
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-between items-center', [
div('flex flex-row gap-12 items-center', [
span('text-lg font-semibold', config.aspects[character.aspect.id]?.name), div('flex flex-row items-center gap-2', [ text('Transformé'), checkbox({ defaultValue: character.variables.transformed, change: v => character.variables.transformed = v, }) ]),
]),
div('flex flex-row gap-8 items-center', [
text('Difficulté: '), span('text-lg font-semibold', config.aspects[character.aspect.id]?.difficulty),
]),
]),
div(() => ({ 'opacity-20': !character.variables.transformed }), [ markdown(getText(config.aspects[character.aspect.id]?.description), undefined, { tags: { a: preview } }), ]),
])
]
}
effectsTab(character: CompiledCharacter)