You've already forked obsidian-visualiser
Implement Aspect tab and HP/Mana editor
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user