Implement Aspect tab and HP/Mana editor
This commit is contained in:
parent
a412116b9c
commit
3081c05b55
|
|
@ -57,6 +57,7 @@ export type CharacterVariables = {
|
||||||
items: ItemState[];
|
items: ItemState[];
|
||||||
|
|
||||||
money: number;
|
money: number;
|
||||||
|
transformed: boolean;
|
||||||
};
|
};
|
||||||
export type TreeLeaf = {
|
export type TreeLeaf = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { eq } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterTable } from '~/db/schema';
|
import { characterTable } from '~/db/schema';
|
||||||
import { CharacterVariablesValidation } from '~~/shared/character';
|
import { CharacterVariablesValidation } from '~~/shared/character';
|
||||||
import type { CharacterVariables } from '~/types/character';
|
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const id = getRouterParam(e, "id");
|
const id = getRouterParam(e, "id");
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from "zod/v4";
|
||||||
import type { User } from "~/types/auth";
|
import type { User } from "~/types/auth";
|
||||||
import characterConfig from '#shared/character-config.json';
|
import characterConfig from '#shared/character-config.json';
|
||||||
import type { Campaign } from "~/types/campaign";
|
import type { Campaign } from "~/types/campaign";
|
||||||
import { div, dom, icon, span, text, type HTMLElement } from "#shared/dom";
|
import { div, dom, icon, span, text } from "#shared/dom";
|
||||||
import { button, foldable, loading, numberpicker, tabgroup, Toaster } from "#shared/components";
|
import { button, foldable, loading, numberpicker, tabgroup, Toaster } from "#shared/components";
|
||||||
import { CharacterCompiler, colorByRarity, stateFactory, subnameFactory } from "#shared/character";
|
import { CharacterCompiler, colorByRarity, stateFactory, subnameFactory } from "#shared/character";
|
||||||
import { modal, tooltip } from "#shared/floating";
|
import { modal, tooltip } from "#shared/floating";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
|
import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
|
||||||
import { clamp, lerp } from "#shared/general";
|
import { clamp, lerp } from "#shared/general";
|
||||||
import { dom, icon, svg, type HTMLElement } from "#shared/dom";
|
import { dom, icon, svg } from "#shared/dom";
|
||||||
import render from "#shared/markdown";
|
import render from "#shared/markdown";
|
||||||
import { tooltip } from "#shared/floating";
|
import { tooltip } from "#shared/floating";
|
||||||
import { History } from "#shared/history";
|
import { History } from "#shared/history";
|
||||||
|
|
|
||||||
|
|
@ -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, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components";
|
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 { followermenu, fullblocker, tooltip } from "#shared/floating";
|
||||||
import { clamp } from "#shared/general";
|
import { clamp } from "#shared/general";
|
||||||
import markdown from "#shared/markdown";
|
import markdown from "#shared/markdown";
|
||||||
|
|
@ -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";
|
import { MarkdownEditor } from "#shared/editor";
|
||||||
import { Socket } from "#shared/websocket";
|
import { Socket } from "#shared/websocket";
|
||||||
import { raw, reactive } from '#shared/reactive';
|
import { raw, reactive, reactivity } from '#shared/reactive';
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
|
|
||||||
|
|
@ -47,6 +47,7 @@ export const defaultCharacter: Character = {
|
||||||
sickness: [],
|
sickness: [],
|
||||||
poisons: [],
|
poisons: [],
|
||||||
money: 0,
|
money: 0,
|
||||||
|
transformed: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
owner: -1,
|
owner: -1,
|
||||||
|
|
@ -259,6 +260,7 @@ export const CharacterVariablesValidation = z.object({
|
||||||
items: z.array(ItemStateValidation),
|
items: z.array(ItemStateValidation),
|
||||||
|
|
||||||
money: z.number(),
|
money: z.number(),
|
||||||
|
transformed: z.boolean(),
|
||||||
});
|
});
|
||||||
export const CharacterValidation = z.object({
|
export const CharacterValidation = z.object({
|
||||||
id: z.number(),
|
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]]!));
|
Object.entries(value.leveling).forEach(e => this.add(config.peoples[value.people!]!.options[parseInt(e[0]) as Level][e[1]]!));
|
||||||
|
|
||||||
MAIN_STATS.forEach(stat => {
|
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.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
|
get character(): Character
|
||||||
|
|
@ -385,8 +407,8 @@ export class CharacterCompiler
|
||||||
}
|
}
|
||||||
get armor()
|
get armor()
|
||||||
{
|
{
|
||||||
const armors = this._character.variables.items.filter(e => e.equipped && config.items[e.id]?.category === 'armor');
|
const armor = this._character.variables.items.find(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;
|
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()
|
get weight()
|
||||||
{
|
{
|
||||||
|
|
@ -1573,6 +1595,8 @@ export class CharacterSheet
|
||||||
const publicNotes = new MarkdownEditor();
|
const publicNotes = new MarkdownEditor();
|
||||||
const privateNotes = new MarkdownEditor();
|
const privateNotes = new MarkdownEditor();
|
||||||
|
|
||||||
|
const healthPanel = this.healthPanel(character);
|
||||||
|
|
||||||
const loadableIcon = icon('radix-icons:paper-plane', { width: 16, height: 16 });
|
const loadableIcon = icon('radix-icons:paper-plane', { width: 16, height: 16 });
|
||||||
const saveLoading = loading('small');
|
const saveLoading = loading('small');
|
||||||
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.character?.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
|
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!;
|
publicNotes.content = this.character!.character.notes!.public!;
|
||||||
privateNotes.content = this.character!.character.notes!.private!;
|
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);
|
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.value = (character[property] - character.variables[property]).toString();
|
||||||
obj.edit.replaceWith(obj.readonly);
|
obj.edit.replaceWith(obj.readonly);
|
||||||
|
|
@ -1608,7 +1632,7 @@ export class CharacterSheet
|
||||||
edit: input('text', { defaultValue: (character.mana - character.variables.mana).toString(), input: (v) => {
|
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));
|
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' }),
|
}, 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([
|
this.tabs = tabgroup([
|
||||||
{ id: 'actions', title: [ text('Actions') ], content: this.actionsTab(character) },
|
{ 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: '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) },
|
{ id: 'effects', title: [ text('Afflictions') ], content: () => this.effectsTab(character) },
|
||||||
|
|
||||||
|
|
@ -1633,7 +1657,6 @@ export class CharacterSheet
|
||||||
[ span('text-lg font-bold', 'Notes privés'), ], {
|
[ 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
|
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; } });
|
], { 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", [
|
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" }, [
|
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||||
text("PV: "),
|
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),
|
text(() => character.health),
|
||||||
]),
|
]),
|
||||||
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
|
||||||
text("Mana: "),
|
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),
|
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 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 2xl:gap-4 gap-2 flex-row items-center justify-between", [
|
||||||
div("flex flex-col items-center px-2", [
|
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" })
|
dom("span", { class: "text-sm 2xl:text-base", text: "Force" })
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
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é" })
|
dom("span", { class: "text-sm 2xl:text-base", text: "Dextérité" })
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
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" })
|
dom("span", { class: "text-sm 2xl:text-base", text: "Constitution" })
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
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" })
|
dom("span", { class: "text-sm 2xl:text-base", text: "Intelligence" })
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
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é" })
|
dom("span", { class: "text-sm 2xl:text-base", text: "Curiosité" })
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
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" })
|
dom("span", { class: "text-sm 2xl:text-base", text: "Charisme" })
|
||||||
]),
|
]),
|
||||||
div("flex flex-col items-center px-2", [
|
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é" })
|
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)
|
actionsTab(character: CompiledCharacter)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
@ -2222,7 +2333,17 @@ export class CharacterSheet
|
||||||
aspectTab(character: CompiledCharacter)
|
aspectTab(character: CompiledCharacter)
|
||||||
{
|
{
|
||||||
return [
|
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)
|
effectsTab(character: CompiledCharacter)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
|
||||||
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node, type HTMLElement } from "#shared/dom";
|
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "#shared/dom";
|
||||||
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "#shared/floating";
|
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "#shared/floating";
|
||||||
import { clamp } from "#shared/general";
|
import { clamp } from "#shared/general";
|
||||||
import { Tree } from "#shared/tree";
|
import { Tree } from "#shared/tree";
|
||||||
import type { Placement } from "@floating-ui/dom";
|
import type { Placement } from "@floating-ui/dom";
|
||||||
import { type Reactive } from '#shared/reactive';
|
import { reactivity, type Reactive } from '#shared/reactive';
|
||||||
|
|
||||||
export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
|
export function link(children: NodeChildren, properties?: NodeProperties & { active?: Class }, link?: RouteLocationAsRelativeTyped<RouteMapGeneric, string>)
|
||||||
{
|
{
|
||||||
|
|
@ -394,9 +394,9 @@ export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', se
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
export function numberpicker(settings?: { defaultValue?: number, change?: (value: number) => void, input?: (value: number) => void, focus?: (value: number) => void, blur?: (value: number) => void, class?: Class, min?: number, max?: number, disabled?: boolean }): HTMLInputElement
|
export function numberpicker(settings?: { defaultValue?: Reactive<number>, change?: (value: number) => void, input?: (value: number) => void, focus?: (value: number) => void, blur?: (value: number) => void, class?: Class, min?: number, max?: number, disabled?: Reactive<boolean> }): HTMLInputElement
|
||||||
{
|
{
|
||||||
let storedValue = settings?.defaultValue ?? 0;
|
let storedValue = settings?.defaultValue ? typeof settings.defaultValue === 'function' ? settings.defaultValue() : settings.defaultValue : 0;
|
||||||
const validateAndChange = (value: number) => {
|
const validateAndChange = (value: number) => {
|
||||||
if(isNaN(value))
|
if(isNaN(value))
|
||||||
field.value = '';
|
field.value = '';
|
||||||
|
|
@ -412,7 +412,7 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 disabled:shadow-none disabled:bg-light-20 dark:disabled:bg-dark-20 disabled:border-dashed disabled:border-light-30 dark:disabled:border-dark-30`, settings?.class], listeners: {
|
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 disabled:shadow-none disabled:bg-light-20 dark:disabled:bg-dark-20 disabled:border-dashed disabled:border-light-35 dark:disabled:border-dark-35 disabled:border-2`, settings?.class], listeners: {
|
||||||
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
||||||
keydown: (e: KeyboardEvent) => {
|
keydown: (e: KeyboardEvent) => {
|
||||||
if(field.disabled)
|
if(field.disabled)
|
||||||
|
|
@ -443,12 +443,18 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
|
||||||
focus: () => settings?.focus && settings.focus(storedValue),
|
focus: () => settings?.focus && settings.focus(storedValue),
|
||||||
blur: () => settings?.blur && settings.blur(storedValue),
|
blur: () => settings?.blur && settings.blur(storedValue),
|
||||||
}});
|
}});
|
||||||
if(settings?.defaultValue !== undefined) field.value = storedValue.toString(10);
|
if(settings?.defaultValue !== undefined)
|
||||||
|
{
|
||||||
|
reactivity(settings.defaultValue, (v) => {
|
||||||
|
field.value = v.toString(10);
|
||||||
|
storedValue = v;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
// Open by default
|
// Open by default
|
||||||
export function foldable(content: Reactive<NodeChildren>, title: NodeChildren, settings?: { open?: boolean, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
|
export function foldable(content: Reactive<NodeChildren>, title: NodeChildren, settings?: { open?: boolean, onFold?: (state: boolean) => void, class?: { container?: Class, title?: Class, content?: Class, icon?: Class } })
|
||||||
{
|
{
|
||||||
let lazyContent: NodeChildren;
|
let lazyContent: NodeChildren;
|
||||||
const display = (state: boolean) => {
|
const display = (state: boolean) => {
|
||||||
|
|
@ -460,6 +466,7 @@ export function foldable(content: Reactive<NodeChildren>, title: NodeChildren, s
|
||||||
lazyContent && contentContainer.replaceChildren(...lazyContent.map(e => typeof e ==='function' ? e() : e).filter(e => !!e));
|
lazyContent && contentContainer.replaceChildren(...lazyContent.map(e => typeof e ==='function' ? e() : e).filter(e => !!e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
settings?.onFold && settings.onFold(state);
|
||||||
}
|
}
|
||||||
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
|
const contentContainer = div(['hidden group-data-[active]:flex', settings?.class?.content]);
|
||||||
const fold = div(['group flex w-full flex-col', settings?.class?.container], [
|
const fold = div(['group flex w-full flex-col', settings?.class?.container], [
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { safeDestr as parse } from 'destr';
|
||||||
import { Canvas, CanvasEditor } from "#shared/canvas";
|
import { Canvas, CanvasEditor } from "#shared/canvas";
|
||||||
import render, { renderMDAsText } from "#shared/markdown";
|
import render, { renderMDAsText } from "#shared/markdown";
|
||||||
import { confirm, contextmenu, tooltip } from "#shared/floating";
|
import { confirm, contextmenu, tooltip } from "#shared/floating";
|
||||||
import { cancelPropagation, dom, icon, text, type Node, type HTMLElement } from "#shared/dom";
|
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom";
|
||||||
import { loading } from "#shared/components";
|
import { loading } from "#shared/components";
|
||||||
import prose, { h1, h2 } from "#shared/proses";
|
import prose, { h1, h2 } from "#shared/proses";
|
||||||
import { getID, lerp, parsePath } from '~~/shared/general';
|
import { getID, lerp, parsePath } from '~~/shared/general';
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { buildIcon, getIcon, iconLoaded, loadIcon, type IconifyIcon } from 'icon
|
||||||
import { loading } from './components';
|
import { loading } from './components';
|
||||||
import { _defer, raw, reactivity, type Proxy, type Reactive } from './reactive';
|
import { _defer, raw, reactivity, type Proxy, type Reactive } from './reactive';
|
||||||
|
|
||||||
export type HTMLElement = HTMLElement;
|
|
||||||
export type Node = HTMLElement | SVGElement | Text | undefined;
|
export type Node = HTMLElement | SVGElement | Text | undefined;
|
||||||
export type NodeChildren = Array<Reactive<Node>> | undefined;
|
export type NodeChildren = Array<Reactive<Node>> | undefined;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap, c
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
|
import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
import { div, dom, icon, span, type HTMLElement } from '~~/shared/dom';
|
import { div, dom, icon, span } from '~~/shared/dom';
|
||||||
import { callout as calloutExtension, calloutKeymap } from '#shared/grammar/callout.extension';
|
import { callout as calloutExtension, calloutKeymap } from '#shared/grammar/callout.extension';
|
||||||
import { wikilink as wikilinkExtension, autocompletion as wikilinkAutocompletion } from '#shared/grammar/wikilink.extension';
|
import { wikilink as wikilinkExtension, autocompletion as wikilinkAutocompletion } from '#shared/grammar/wikilink.extension';
|
||||||
import renderMarkdown from '~~/shared/markdown';
|
import renderMarkdown from '~~/shared/markdown';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character";
|
import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character";
|
||||||
import { div, dom, icon, span, text, type NodeChildren, type HTMLElement } from "#shared/dom";
|
import { div, dom, icon, span, text, type NodeChildren } from "#shared/dom";
|
||||||
import { MarkdownEditor } from "#shared/editor";
|
import { MarkdownEditor } from "#shared/editor";
|
||||||
import { preview } from "#shared/proses";
|
import { preview } from "#shared/proses";
|
||||||
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components";
|
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import * as FloatingUI from "@floating-ui/dom";
|
import * as FloatingUI from "@floating-ui/dom";
|
||||||
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren, type HTMLElement } from "./dom";
|
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren } from "./dom";
|
||||||
import { button } from "./components";
|
import { button } from "./components";
|
||||||
import type { Reactive } from "./reactive";
|
import type { Reactive } from "./reactive";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Content, type LocalContent } from "./content";
|
import { Content, type LocalContent } from "./content";
|
||||||
import { dom, type HTMLElement } from "./dom";
|
import { dom } from "./dom";
|
||||||
import { clamp } from "./general";
|
import { clamp } from "./general";
|
||||||
|
|
||||||
export type Recursive<T> = T & {
|
export type Recursive<T> = T & {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue