Add back Loading Indicator, rework children caching, small visual improvement on character sheet and config management.

This commit is contained in:
Clément Pons 2026-01-12 17:48:28 +01:00
parent 0eaffcaa04
commit f761e44569
16 changed files with 333 additions and 238 deletions

View File

@ -1,13 +1,12 @@
<template> <template>
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden"> <div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
<NuxtRouteAnnouncer/> <NuxtRouteAnnouncer/>
<TooltipProvider> <NuxtLoadingIndicator :throttle="50"/>
<NuxtLayout> <NuxtLayout>
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer"> <div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer">
<NuxtPage /> <NuxtPage />
</div> </div>
</NuxtLayout> </NuxtLayout>
</TooltipProvider>
</div> </div>
</template> </template>

View File

@ -39,7 +39,7 @@ function create()
useRequestFetch()('/api/campaign', { useRequestFetch()('/api/campaign', {
method: 'POST', method: 'POST',
body: { name: 'Margooning', public_notes: '', dm_notes: '', settings: {} }, body: { name: 'Margooning', public_notes: '', dm_notes: '', settings: {} },
}).then(() => Toaster.add({ duration: 8000, content: 'Campagne créée', type: 'info' })).catch((e) => Toaster.add({ duration: 8000, title: 'Une erreur est survenue', content: e, type: 'error' })) }).then((result) => Toaster.add({ duration: 8000, content: 'Campagne créée', type: 'info' })).catch((e) => Toaster.add({ duration: 8000, title: 'Une erreur est survenue', content: e, type: 'error' }))
} }
</script> </script>
@ -87,20 +87,6 @@ function create()
</AlertDialogRoot> </AlertDialogRoot>
</div> </div>
</div> </div>
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="campaign of campaigns.filter(e => e.status === 'ARCHIVED')">
<NuxtLink :to="{ name: 'campaign-id', params: { id: campaign.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
<div class="flex flex-row gap-8 ps-4 items-center">
<div class="flex flex-1 flex-col gap-2 justify-center">
<span class="text-lg font-bold group-hover:text-accent-blue">{{ campaign.name }}</span>
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
<div class="flex flex-row flex-1 items-stretch gap-4">
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
</div>
</div>
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
</div>
</NuxtLink>
</div>
</div> </div>
<div v-if="archives && archives.length > 0" class="flex flex-row w-full gap-8 justify-center items-center"><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span><span class="text-lg font-semibold">Archives</span><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span></div> <div v-if="archives && archives.length > 0" class="flex flex-row w-full gap-8 justify-center items-center"><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span><span class="text-lg font-semibold">Archives</span><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span></div>
<div v-if="archives && archives.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full"> <div v-if="archives && archives.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
@ -136,20 +122,6 @@ function create()
</AlertDialogRoot> </AlertDialogRoot>
</div> </div>
</div> </div>
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="campaign of campaigns.filter(e => e.status === 'ARCHIVED')">
<NuxtLink :to="{ name: 'campaign-id', params: { id: campaign.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
<div class="flex flex-row gap-8 ps-4 items-center">
<div class="flex flex-1 flex-col gap-2 justify-center">
<span class="text-lg font-bold group-hover:text-accent-blue">{{ campaign.name }}</span>
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
<div class="flex flex-row flex-1 items-stretch gap-4">
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
</div>
</div>
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
</div>
</NuxtLink>
</div>
</div> </div>
</div> </div>
<div v-else class="flex flex-col gap-2 items-center flex-1"> <div v-else class="flex flex-col gap-2 items-center flex-1">

View File

@ -1,4 +1,4 @@
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES, DAMAGE_TYPES, WEAPON_TYPES } from "#shared/character.util"; import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES, DAMAGE_TYPES, WEAPON_TYPES, PropertySum, ITEM_BUFFER_KEYS } from "#shared/character.util";
import type { Localized } from "../types/general"; import type { Localized } from "../types/general";
export type MainStat = typeof MAIN_STATS[number]; export type MainStat = typeof MAIN_STATS[number];
@ -57,11 +57,16 @@ export type CharacterVariables = {
money: number; money: number;
}; };
export type TreeStructure = {
name: string;
};
type CommonState = { type CommonState = {
capacity?: number; capacity?: number;
powercost?: number; powercost?: number;
}; };
type ArmorState = { health?: number }; type StateBufferKeys = typeof ITEM_BUFFER_KEYS[number];
type ArmorState = { loss: number, health?: number, absorb?: { flat?: number, percent?: number } };
type WeaponState = { attack?: number | string, hit?: number }; type WeaponState = { attack?: number | string, hit?: number };
type WondrousState = { }; type WondrousState = { };
type MundaneState = { }; type MundaneState = { };
@ -72,11 +77,12 @@ type ItemState = {
charges?: number; charges?: number;
equipped?: boolean; equipped?: boolean;
state?: (ArmorState | WeaponState | WondrousState | MundaneState) & CommonState; state?: (ArmorState | WeaponState | WondrousState | MundaneState) & CommonState;
buffer?: Partial<Record<StateBufferKeys, PropertySum>>;
}; };
export type CharacterConfig = { export type CharacterConfig = {
peoples: Record<string, RaceConfig>; peoples: Record<string, RaceConfig>;
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>; training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
spells: Record<string, SpellConfig | ArtConfig>; spells: Record<string, SpellConfig>;
aspects: Record<string, AspectConfig>; aspects: Record<string, AspectConfig>;
features: Record<FeatureID, Feature>; features: Record<FeatureID, Feature>;
enchantments: Record<string, EnchantementConfig>; enchantments: Record<string, EnchantementConfig>;
@ -86,11 +92,12 @@ export type CharacterConfig = {
freeaction: Record<string, { id: string, name: string, description: string }>; freeaction: Record<string, { id: string, name: string, description: string }>;
passive: Record<string, { id: string, name: string, description: string }>; passive: Record<string, { id: string, name: string, description: string }>;
texts: Record<i18nID, Localized>; texts: Record<i18nID, Localized>;
trees: Record<string, TreeStructure>;
//Each of these groups extend an existing feature as they all use the same properties //Each of these groups extend an existing feature as they all use the same properties
sickness: Record<FeatureID, { stage: number }>; //TODO sickness: Record<FeatureID, { name: string, stage: number }>; //TODO
poisons: Record<FeatureID, { difficulty: number, efficienty: number, solubility: number }>; //TODO poisons: Record<FeatureID, { name: string, difficulty: number, efficienty: number, solubility: number }>; //TODO
dedications: Record<FeatureID, { id: string, name: string, description: i18nID, effect: FeatureID[], requirement: Array<{ stat: MainStat, amount: number }> }>; //TODO dedications: Record<FeatureID, { name: string, requirement: Array<{ stat: MainStat, amount: number }> }>; //TODO
}; };
export type EnchantementConfig = { export type EnchantementConfig = {
id: string; id: string;
@ -142,7 +149,7 @@ export type SpellConfig = {
id: string; id: string;
name: string; //TODO -> TextID name: string; //TODO -> TextID
rank: 1 | 2 | 3 | 4; rank: 1 | 2 | 3 | 4;
type: Exclude<SpellType, "arts">; type: SpellType;
cost: number; cost: number;
speed: "action" | "reaction" | number; speed: "action" | "reaction" | number;
elements: Array<SpellElement>; elements: Array<SpellElement>;
@ -191,7 +198,7 @@ export type FeatureEquipment = {
id: FeatureID; id: FeatureID;
category: "value"; category: "value";
operation: "add" | "set" | "min"; operation: "add" | "set" | "min";
property: 'weapon/damage/value' | 'armor/health' | 'armor/absorb/flat' | 'armor/absorb/percent'; property: StateBufferKeys;
value: number | `modifier/${MainStat}` | false; value: number | `modifier/${MainStat}` | false;
} }
export type FeatureList = { export type FeatureList = {
@ -228,13 +235,15 @@ export type CompiledCharacter = {
race: string; race: string;
spellslots: number; //Max spellslots: number; //Max
artslots: number; //Max artslots: number; //Max
spellranks: Record<SpellType, 0 | 1 | 2 | 3>; spellranks: Record<SpellType | 'arts', 0 | 1 | 2 | 3>;
aspect: { aspect: {
id: string, id: string,
amount: number; amount: number;
duration: number; duration: number;
bonus: number; shift_bonus: number;
tier: 0 | 1 | 2; tier: 0 | 1 | 2;
bonus?: Partial<CompiledCharacter['bonus']>;
}; };
speed: number | false; speed: number | false;
capacity: number | false; capacity: number | false;
@ -269,13 +278,13 @@ export type CompiledCharacter = {
defense: Partial<Record<MainStat, number>>; //Defense aux jets de resistance defense: Partial<Record<MainStat, number>>; //Defense aux jets de resistance
abilities: Partial<Record<Ability, number>>; abilities: Partial<Record<Ability, number>>;
spells: { spells: {
type: Partial<Record<SpellType, number>>; type: Partial<Record<SpellType | 'arts', number>>;
rank: Partial<Record<1 | 2 | 3 | 4, number>>; rank: Partial<Record<1 | 2 | 3 | 4, number>>;
elements: Partial<Record<SpellElement, number>>; elements: Partial<Record<SpellElement, number>>;
}; };
weapon: Partial<Record<WeaponType, number>>; weapon: Partial<Record<WeaponType, number>>;
}; //Any special bonus goes here
resistance: Partial<Record<Resistance, number>>; //Bonus à l'attaque resistance: Partial<Record<Resistance, number>>; //Bonus à l'attaque
}; //Any special bonus goes here
craft: { level: number, bonus: number }; craft: { level: number, bonus: number };

BIN
db.sqlite

Binary file not shown.

View File

@ -199,5 +199,8 @@ export default defineNuxtConfig({
compilerOptions: { compilerOptions: {
isCustomElement: (tag) => tag === 'iconify-icon', isCustomElement: (tag) => tag === 'iconify-icon',
} }
},
devtools: {
enabled: false,
} }
}) })

View File

@ -10,7 +10,7 @@ export default defineEventHandler(async (e) => {
if(!body.success) if(!body.success)
{ {
setResponseStatus(e, 400); setResponseStatus(e, 400);
return body.error.message; throw body.error.message;
} }
const session = await getUserSession(e); const session = await getUserSession(e);
@ -39,7 +39,7 @@ export default defineEventHandler(async (e) => {
}); });
setResponseStatus(e, 201); setResponseStatus(e, 201);
return id; return { id, link: cryptURI('campaign', id) };
} }
catch(_e) catch(_e)
{ {

View File

@ -162,7 +162,7 @@ export class CampaignSheet
div('flex flex-row gap-2 items-center py-2', [ div('flex flex-row gap-2 items-center py-2', [
div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), campaign.owner.username, 'bottom') ]), div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), campaign.owner.username, 'bottom') ]),
div('border-l h-full w-0 border-light-40 dark:border-dark-40'), div('border-l h-full w-0 border-light-40 dark:border-dark-40'),
div('flex flex-row gap-1', { list: campaign.members, render: (member) => div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), member.member.username, 'bottom') ]) }), div('flex flex-row gap-1', { list: campaign.members, render: (member, _c) => _c ?? div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), member.member.username, 'bottom') ]) }),
]), ]),
div('flex flex-1 flex-col items-center justify-center gap-2', [ div('flex flex-1 flex-col items-center justify-center gap-2', [
span('text-2xl font-serif font-bold italic', campaign.name), span('text-2xl font-serif font-bold italic', campaign.name),
@ -178,7 +178,7 @@ export class CampaignSheet
div('flex flex-row gap-4 flex-1 h-0', [ div('flex flex-row gap-4 flex-1 h-0', [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4 w-[320px]', [ span('font-bold text-lg', 'Etat'), div('border-t border-light-40 dark:border-dark-40 border-dashed flex-1') ]), div('flex flex-row items-center gap-4 w-[320px]', [ span('font-bold text-lg', 'Etat'), div('border-t border-light-40 dark:border-dark-40 border-dashed flex-1') ]),
div('flex flex-col gap-2', { list: this.characters, render: (e) => e.container }), div('flex flex-col gap-2', { list: this.characters, render: (e, _c) => _c ?? e.container }),
div('px-8 py-4 w-full flex', [ div('px-8 py-4 w-full flex', [
button([ button([
icon('radix-icons:plus-circled', { width: 24, height: 24 }), icon('radix-icons:plus-circled', { width: 24, height: 24 }),
@ -218,7 +218,7 @@ export class CampaignSheet
const _modal = modal([ const _modal = modal([
div('flex flex-col gap-4 items-center min-w-[480px] min-h-24', [ div('flex flex-col gap-4 items-center min-w-[480px] min-h-24', [
span('text-xl font-bold', 'Mes personnages'), span('text-xl font-bold', 'Mes personnages'),
div('grid grid-cols-3 gap-2', { list: () => current.characters, render: (e) => div('border border-light-40 dark:border-dark-40 p-2 flex flex-col w-[140px]', [ div('grid grid-cols-3 gap-2', { list: () => current.characters, render: (e, _c) => _c ?? div('border border-light-40 dark:border-dark-40 p-2 flex flex-col w-[140px]', [
span('font-bold', e.name), span('font-bold', e.name),
span('', `Niveau ${e.level}`), span('', `Niveau ${e.level}`),
button(text('Ajouter'), () => useRequestFetch()(`/api/character/${e.id}/campaign/${this.campaign!.id}`, { method: 'POST' }).then(() => this.ws!.send('character', { id: e.id, name: e.name, action: 'ADD', })).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(_modal.close)), button(text('Ajouter'), () => useRequestFetch()(`/api/character/${e.id}/campaign/${this.campaign!.id}`, { method: 'POST' }).then(() => this.ws!.send('character', { id: e.id, name: e.name, action: 'ADD', })).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(_modal.close)),
@ -257,7 +257,9 @@ export class CampaignSheet
div('flex flex-row gap-1 items-center', [ span('italic text-sm', 'Argent'), this.user.value && this.user.value.id === this.campaign.owner.id ? money.readonly : div('cursor-pointer px-2 py-px flex flex-row gap-1 items-center', [ span('text-lg font-bold', () => this.campaign!.money.toLocaleString(undefined, { useGrouping: true })), icon('ph:coin', { width: 16, height: 16 }) ]) ]), div('flex flex-row gap-1 items-center', [ span('italic text-sm', 'Argent'), this.user.value && this.user.value.id === this.campaign.owner.id ? money.readonly : div('cursor-pointer px-2 py-px flex flex-row gap-1 items-center', [ span('text-lg font-bold', () => this.campaign!.money.toLocaleString(undefined, { useGrouping: true })), icon('ph:coin', { width: 16, height: 16 }) ]) ]),
]) ])
]), ]),
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: this.campaign.items, render: e => { div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: this.campaign.items, render: (e, _c) => {
if(_c) return _c;
const item = config.items[e.id]; const item = config.items[e.id];
if(!item) return; if(!item) return;

View File

@ -200,15 +200,13 @@ export class Node extends EventTarget
{ {
properties: CanvasNode; properties: CanvasNode;
nodeDom!: RedrawableHTML; nodeDom?: RedrawableHTML;
constructor(properties: CanvasNode) constructor(properties: CanvasNode)
{ {
super(); super();
this.properties = properties; this.properties = properties;
this.getDOM()
} }
protected getDOM() protected getDOM()
@ -230,6 +228,13 @@ export class Node extends EventTarget
} }
} }
get dom()
{
if(this.nodeDom === undefined)
this.getDOM();
return this.nodeDom;
}
get style() get style()
{ {
return this.properties.color ? this.properties.color?.class ? return this.properties.color ? this.properties.color?.class ?
@ -270,7 +275,7 @@ export class NodeEditable extends Node
if(!this.dirty) if(!this.dirty)
return; return;
Object.assign(this.nodeDom.style, { Object.assign(this.nodeDom!.style, {
transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, transform: `translate(${this.properties.x}px, ${this.properties.y}px)`,
width: `${this.properties.width}px`, width: `${this.properties.width}px`,
height: `${this.properties.height}px`, height: `${this.properties.height}px`,
@ -329,7 +334,7 @@ export class Edge extends EventTarget
{ {
properties: CanvasEdge; properties: CanvasEdge;
edgeDom!: RedrawableHTML; edgeDom?: RedrawableHTML;
protected from: Node; protected from: Node;
protected to: Node; protected to: Node;
protected path: Path; protected path: Path;
@ -344,8 +349,6 @@ export class Edge extends EventTarget
this.to = to; this.to = to;
this.path = getPath(this.from.properties, properties.fromSide, this.to.properties, properties.toSide)!; this.path = getPath(this.from.properties, properties.fromSide, this.to.properties, properties.toSide)!;
this.labelPos = labelCenter(this.from.properties, properties.fromSide, this.to.properties, properties.toSide); this.labelPos = labelCenter(this.from.properties, properties.fromSide, this.to.properties, properties.toSide);
this.getDOM();
} }
protected getDOM() protected getDOM()
@ -364,6 +367,13 @@ export class Edge extends EventTarget
]); ]);
} }
get dom()
{
if(this.edgeDom === undefined)
this.getDOM();
return this.edgeDom;
}
get style() get style()
{ {
return this.properties.color ? this.properties.color?.class ? return this.properties.color ? this.properties.color?.class ?
@ -377,8 +387,8 @@ export class EdgeEditable extends Edge
private focusing: boolean = false; private focusing: boolean = false;
private editing: boolean = false; private editing: boolean = false;
private pathDom!: SVGPathElement; private pathDom?: SVGPathElement;
private inputDom!: RedrawableHTML; private inputDom?: RedrawableHTML;
constructor(properties: CanvasEdge, from: NodeEditable, to: NodeEditable) constructor(properties: CanvasEdge, from: NodeEditable, to: NodeEditable)
{ {
super(properties, from, to); super(properties, from, to);
@ -406,7 +416,7 @@ export class EdgeEditable extends Edge
update() update()
{ {
this.path = getPath(this.from.properties, this.properties.fromSide, this.to.properties, this.properties.toSide)!; this.path = getPath(this.from.properties, this.properties.fromSide, this.to.properties, this.properties.toSide)!;
this.pathDom.setAttribute('d', this.path.path); this.pathDom!.setAttribute('d', this.path.path);
} }
} }
@ -469,7 +479,7 @@ export class Canvas
//const { loggedIn, user } = useUserSession(); //const { loggedIn, user } = useUserSession();
this.transform = dom('div', { class: 'origin-center h-full' }, [ this.transform = dom('div', { class: 'origin-center h-full' }, [
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [ dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
dom('div', {}, this.nodes.map(e => e.nodeDom)), dom('div', {}, this.edges.map(e => e.edgeDom)), dom('div', {}, this.nodes.map(e => e.dom)), dom('div', {}, this.edges.map(e => e.dom)),
]) ])
]); ]);
@ -763,7 +773,7 @@ export class CanvasEditor extends Canvas
this.transform = dom('div', { class: 'origin-center h-full' }, [ this.transform = dom('div', { class: 'origin-center h-full' }, [
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [ dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
dom('div', {}, [...this.nodes.map(e => e.nodeDom), this.nodeHelper]), dom('div', {}, [...this.edges.map(e => e.edgeDom)]), dom('div', {}, [...this.nodes.map(e => e.dom), this.nodeHelper]), dom('div', {}, [...this.edges.map(e => e.dom)]),
]), this.edgeHelper, ]), this.edgeHelper,
]); ]);

File diff suppressed because one or more lines are too long

View File

@ -1,19 +1,17 @@
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, EnchantementConfig, FeatureItem, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character"; import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, EnchantementConfig, FeatureEquipment, FeatureItem, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
import { z } from "zod/v4"; import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json'; import characterConfig from '#shared/character-config.json';
import proses, { a, preview } from "#shared/proses"; import proses, { a, preview } from "#shared/proses";
import { async, 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, optionmenu, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, span, text, 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 } from "#shared/general.util"; import { clamp } from "#shared/general.util";
import markdown, { defaultProses, filterMarkdown, renderMarkdown } from "#shared/markdown.util"; import markdown from "#shared/markdown.util";
import { getText } from "#shared/i18n"; 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 } from '#shared/reactive'; import { raw, reactive } from '#shared/reactive';
import { Content, type ContentMap, type LocalContent } from "./content.util";
import type { Root } from "hast";
const config = characterConfig as CharacterConfig; const config = characterConfig as CharacterConfig;
@ -21,13 +19,14 @@ export const MAIN_STATS = ["strength","dexterity","constitution","intelligence",
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const; export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const;
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const; export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const;
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12] as const; export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12] as const;
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const; export const SPELL_TYPES = ["precision","knowledge","instinct"] as const;
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const; export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const; export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const; export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const;
export const RESISTANCES = ['stun','bleed','poison','fear','influence','charm','possesion','precision','knowledge','instinct'] as const; export const RESISTANCES = ['stun','bleed','poison','fear','influence','charm','possesion','precision','knowledge','instinct'] as const;
export const DAMAGE_TYPES = ['slashing', 'piercing', 'bludgening', 'magic', 'fire', 'thunder', 'cold'] as const; export const DAMAGE_TYPES = ['slashing', 'piercing', 'bludgening', 'magic', 'fire', 'thunder', 'cold'] as const;
export const WEAPON_TYPES = ["light", "shield", "heavy", "classic", "throw", "natural", "twohanded", "finesse", "reach", "projectile"] as const; export const WEAPON_TYPES = ["light", "shield", "heavy", "classic", "throw", "natural", "twohanded", "finesse", "reach", "projectile"] as const;
export const ITEM_BUFFER_KEYS = ['attack', 'hit', 'health', 'absorb/flat', 'absorb/percent'] as const;
export const defaultCharacter: Character = { export const defaultCharacter: Character = {
id: -1, id: -1,
@ -128,9 +127,9 @@ const defaultCompiledCharacter = (character: Character) => ({
type: {}, type: {},
rank: {}, rank: {},
}, },
weapon: {} weapon: {},
},
resistance: {}, resistance: {},
},
initiative: 0, initiative: 0,
capacity: 0, capacity: 0,
lists: { lists: {
@ -144,7 +143,7 @@ const defaultCompiledCharacter = (character: Character) => ({
id: character.aspect ?? "", id: character.aspect ?? "",
duration: 0, duration: 0,
amount: 0, amount: 0,
bonus: 0, shift_bonus: 0,
tier: 0, tier: 0,
}, },
advantages: [], advantages: [],
@ -199,10 +198,10 @@ export const alignmentTexts: Record<Alignment, string> = {
'neutral_evil': 'Neutre mauvais', 'neutral_evil': 'Neutre mauvais',
'chaotic_evil': 'Chaotique mauvais', 'chaotic_evil': 'Chaotique mauvais',
}; };
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" }; export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision" };
export const abilityTexts: Record<Ability, string> = { export const abilityTexts: Record<Ability, string> = {
"athletics": "Athlétisme", "athletics": "Athlétisme",
"acrobatics": "Acrobatique", "acrobatics": "Acrobatisme",
"intimidation": "Intimidation", "intimidation": "Intimidation",
"sleightofhand": "Doigté", "sleightofhand": "Doigté",
"stealth": "Discrétion", "stealth": "Discrétion",
@ -290,9 +289,11 @@ export const CharacterValidation = z.object({
}); });
type Property = { value: number | string | false, id: string, operation: "set" | "add" | "min" }; type Property = { value: number | string | false, id: string, operation: "set" | "add" | "min" };
type PropertySum = { list: Array<Property>, min: number, value: number, _dirty: boolean }; export type PropertySum = { list: Array<Property>, min: number, value: number, _dirty: boolean };
export class CharacterCompiler export class CharacterCompiler
{ {
private _dirty: boolean = true;
protected _character!: Character; protected _character!: Character;
protected _result!: CompiledCharacter; protected _result!: CompiledCharacter;
protected _buffer: Record<string, PropertySum> = { protected _buffer: Record<string, PropertySum> = {
@ -324,6 +325,7 @@ 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: [] },
}; };
this._dirty = true;
if(value.people !== undefined) if(value.people !== undefined)
{ {
@ -334,6 +336,8 @@ export class CharacterCompiler
}); });
Object.entries(value.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]); Object.entries(value.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
value.variables.items.forEach((e) => this.update(e));
} }
} }
get character(): Character get character(): Character
@ -343,7 +347,8 @@ export class CharacterCompiler
get compiled(): CompiledCharacter get compiled(): CompiledCharacter
{ {
Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]); Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
this.compile(Object.keys(this._buffer)); this._dirty && this.compile(Object.keys(this._buffer));
this._dirty = false;
return this._result; return this._result;
} }
@ -352,7 +357,8 @@ export class CharacterCompiler
Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]); Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
const keys = Object.keys(this._buffer); const keys = Object.keys(this._buffer);
this.compile(keys); this._dirty && this.compile(keys);
this._dirty = false;
return keys.reduce((p, v) => { return keys.reduce((p, v) => {
p[v] = this._buffer[v]!.value; p[v] = this._buffer[v]!.value;
@ -362,7 +368,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 as ArmorState)?.health ?? 0) })).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 as ArmorState)?.loss ?? 0) })).reduce((p, v) => { p.max += v.max; p.current += v.current; return p; }, { max: 0, current: 0 }) : undefined;
} }
get weight() get weight()
{ {
@ -373,20 +379,28 @@ export class CharacterCompiler
return this._character.variables.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); return this._character.variables.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);
} }
parse(text: string): string update(item: ItemState)
{ {
return text.replace(/\{(.*?)\}/gmi, (substring: string, group: string) => { item.buffer ??= {};
console.log(substring, group);
return substring; if(item.equipped)
}) [...config.items[item.id]?.effects ?? [], ...item.enchantments?.flatMap(e => config.enchantments[e]?.effect) ?? []]?.forEach(e => this.apply(e, item));
else
[...config.items[item.id]?.effects ?? [], ...item.enchantments?.flatMap(e => config.enchantments[e]?.effect) ?? []]?.forEach(e => this.undo(e, item));
this.compile(Object.keys(item.buffer), item.buffer, item.state);
this.compile(Object.keys(this._buffer));
this.saveVariables();
} }
saveVariables() saveVariables()
{ {
const variables = raw(this._character.variables);
variables.items.forEach(e => delete e.buffer);
clearTimeout(this._variableDebounce); clearTimeout(this._variableDebounce);
this._variableDebounce = setTimeout(() => { 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: 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 });
}) })
@ -406,20 +420,21 @@ export class CharacterCompiler
if(!feature) if(!feature)
return; return;
config.features[feature]?.effect.forEach(this.apply.bind(this)); config.features[feature]?.effect.forEach((effect) => this.apply(effect));
} }
protected remove(feature?: string) protected remove(feature?: string)
{ {
if(!feature) if(!feature)
return; return;
config.features[feature]?.effect.forEach(this.undo.bind(this)); config.features[feature]?.effect.forEach((effect) => this.undo(effect));
} }
protected apply(feature?: FeatureItem) protected apply(feature?: FeatureItem | FeatureEquipment, item?: ItemState)
{ {
if(!feature) if(!feature)
return; return;
this._dirty = true;
switch(feature.category) switch(feature.category)
{ {
case "list": case "list":
@ -430,33 +445,35 @@ export class CharacterCompiler
return; return;
case "value": case "value":
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity }; const target = (item && ITEM_BUFFER_KEYS.includes(feature.property as any) ? item.buffer : this._buffer) as Record<string, PropertySum>;
target[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value }); target[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
this._buffer[feature.property]!.min = -Infinity; target[feature.property]!.min = -Infinity;
this._buffer[feature.property]!._dirty = true; target[feature.property]!._dirty = true;
if(feature.property.startsWith('modifier/')) if(feature.property.startsWith('modifier/'))
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty); Object.values(target).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
return; return;
case "choice": case "choice":
const choice = this._character.choices[feature.id]; const choice = this._character.choices[feature.id];
if(choice) if(choice)
choice.forEach(e => feature.options[e]!.effects.forEach(this.apply.bind(this))); choice.forEach(e => feature.options[e]!.effects.forEach((effect) => this.apply(effect)));
return; return;
default: default:
return; return;
} }
} }
protected undo(feature?: FeatureItem) protected undo(feature?: FeatureItem | FeatureEquipment, item?: ItemState)
{ {
if(!feature) if(!feature)
return; return;
this._dirty = true;
switch(feature.category) switch(feature.category)
{ {
case "list": case "list":
@ -467,40 +484,42 @@ export class CharacterCompiler
return; return;
case "value": case "value":
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity }; const target = (item && ITEM_BUFFER_KEYS.includes(feature.property as any) ? item.buffer : this._buffer) as Record<string, PropertySum>;
target[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id), 1); const idx = target[feature.property]!.list.findIndex(e => e.id === feature.id);
idx !== -1 && target[feature.property]!.list.splice(idx, 1);
this._buffer[feature.property]!.min = -Infinity; target[feature.property]!.min = -Infinity;
this._buffer[feature.property]!._dirty = true; target[feature.property]!._dirty = true;
if(feature.property.startsWith('modifier/')) if(feature.property.startsWith('modifier/'))
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty); Object.values(target).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
return; return;
case "choice": case "choice":
const choice = this._character.choices[feature.id]; const choice = this._character.choices[feature.id];
if(choice) if(choice)
choice.forEach(e => feature.options[e]!.effects.forEach(this.undo.bind(this))); choice.forEach(e => feature.options[e]!.effects.forEach((effect) => this.undo(effect)));
return; return;
default: default:
return; return;
} }
} }
protected compile(queue: string[]) protected compile(queue: string[], _buffer: Record<string, PropertySum> = this._buffer, _target: any = this._result)
{ {
for(let i = 0; i < queue.length; i++) for(let i = 0; i < queue.length; i++)
{ {
if(queue[i] === undefined || queue[i] === "") continue; if(queue[i] === undefined || queue[i] === "") continue;
const property = queue[i]!; const property = queue[i]!;
const buffer = this._buffer[property]; const buffer = _buffer[property];
if(buffer && buffer._dirty === true) if(buffer && buffer._dirty === true)
{ {
let sum = 0, shortcut = false; let sum = 0, stop = false;
for(let j = 0; j < buffer.list.length; j++) for(let j = 0; j < buffer.list.length; j++)
{ {
const item = buffer.list[j]; const item = buffer.list[j];
@ -509,13 +528,14 @@ export class CharacterCompiler
if(typeof item.value === 'string') // Add or set a modifier if(typeof item.value === 'string') // Add or set a modifier
{ {
const modifier = this._buffer[item.value as string]!; const modifier = _buffer[item.value as string]!;
if(modifier._dirty) if(modifier._dirty)
{ {
//Put it back in queue since its dependencies haven't been resolved yet //Put it back in queue since its dependencies haven't been resolved yet
//Also put the dependency itself in the queue to make sure it actually get resolves someday
queue.push(item.value as string); queue.push(item.value as string);
queue.push(property); queue.push(property);
shortcut = true; stop = true;
break; break;
} }
else else
@ -539,17 +559,17 @@ export class CharacterCompiler
} }
} }
if(shortcut === true) if(stop === true)
continue; continue;
const path = property.split("/"); const path = property.split("/");
const object = path.length === 1 ? this._result : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, this._result as any); const object = path.length === 1 ? _target : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, _target as any);
if(object.hasOwnProperty(path.slice(-1)[0]!)) if(object.hasOwnProperty(path.slice(-1)[0]!))
object[path.slice(-1)[0]!] = Math.max(sum, this._buffer[property]!.min); object[path.slice(-1)[0]!] = Math.max(sum, _buffer[property]!.min);
this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min); _buffer[property]!.value = Math.max(sum, _buffer[property]!.min);
this._buffer[property]!._dirty = false; _buffer[property]!._dirty = false;
} }
} }
} }
@ -625,7 +645,7 @@ export class CharacterBuilder extends CharacterCompiler
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } }); this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [ this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [ div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [
div('flex flex-row gap-2', [ floater(tooltip(button(icon('radix-icons:pencil-2', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes publics', 'left'), [ publicNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes publics', position: 'bottom-start' }), floater(tooltip(button(icon('radix-icons:eye-none', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes privés', 'right'), [ privateNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes privés', position: 'bottom-start' }) ]), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div('flex flex-row gap-2', [ tooltip(button(icon("radix-icons:chevron-right", { height: 16, width: 16 }), () => this.next(), 'p-1'), 'Suivant', "bottom"), tooltip(button(icon("radix-icons:paper-plane", { height: 16, width: 16 }), () => this.save(), 'p-1'), 'Enregistrer', "bottom"), tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]), div('flex flex-row gap-2', [ floater(tooltip(button(icon('radix-icons:pencil-2', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes publics', 'left'), [ publicNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', title: 'Notes publics', position: 'bottom-start' }), floater(tooltip(button(icon('radix-icons:eye-none', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes privés', 'right'), [ privateNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', title: 'Notes privés', position: 'bottom-start' }) ]), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div('flex flex-row gap-2', [ tooltip(button(icon("radix-icons:chevron-right", { height: 16, width: 16 }), () => this.next(), 'p-1'), 'Suivant', "bottom"), tooltip(button(icon("radix-icons:paper-plane", { height: 16, width: 16 }), () => this.save(), 'p-1'), 'Enregistrer', "bottom"), tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]),
]), ]),
this._content, this._content,
])); ]));
@ -1317,7 +1337,7 @@ export const stateFactory = (item: ItemConfig) => {
switch(item.category) switch(item.category)
{ {
case 'armor': case 'armor':
state.state = { health: 0 } as ArmorState; state.state = { loss: 0, health: 0, absorb: { flat: 0, percent: 0 } } as ArmorState;
break; break;
case 'mundane': case 'mundane':
state.state = { } as MundaneState; state.state = { } as MundaneState;
@ -1576,50 +1596,50 @@ export class CharacterSheet
div("flex flex-col gap-4 py-1 w-60", [ div("flex flex-col gap-4 py-1 w-60", [
div("flex flex-col py-1 gap-4", [ div("flex flex-col py-1 gap-4", [
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]), dom("div", { class: 'text-xl font-semibold', text: "Compétences" }),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
]), ]),
div("grid grid-cols-2 gap-2", div("grid grid-cols-2 gap-2",
Object.keys(character.abilities).map((ability) => Object.keys(character.abilities).map((ability) =>
div("flex flex-row px-1 justify-between items-center", [ div("flex flex-row px-1 justify-between items-center", [
span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate", abilityTexts[ability as Ability] || ability), proses('a', preview, [ span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline", abilityTexts[ability as Ability] || ability) ], { href: `regles/l'entrainement/competences#${abilityTexts[ability as Ability]}`, label: abilityTexts[ability as Ability], navigate: false }),
span("font-bold text-base text-light-100 dark:text-dark-100", text(() => `+${character.abilities[ability as Ability] ?? 0}`)), span("font-bold text-base text-light-100 dark:text-dark-100", text(() => `+${character.abilities[ability as Ability] ?? 0}`)),
]) ])
) )
), ),
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]), dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
]), ]),
() => character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ () => character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère', }) : undefined, () => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline', }) : undefined,
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet' }) : undefined, () => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle' }) : undefined, () => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard' }) : undefined, () => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', label: 'Arme improvisée' }) : undefined, () => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', label: 'Arme improvisée', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', label: 'Arme lourde' }) : undefined, () => character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', label: 'Arme lourde', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', label: 'Arme à deux mains' }) : undefined, () => character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', label: 'Arme à deux mains', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', label: 'Arme maniable' }) : undefined, () => character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', label: 'Arme maniable', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', label: 'Arme à projectiles' }) : undefined, () => character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', label: 'Arme à projectiles', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', label: 'Arme longue' }) : undefined, () => character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', label: 'Arme longue', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier' }) : undefined, () => character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', label: 'Bouclier à deux mains' }) : undefined, () => character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', label: 'Bouclier à deux mains', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
]) : undefined, ]) : undefined,
() => character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ () => character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
() => character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', label: 'Armure légère' }) : undefined, () => character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', label: 'Armure légère', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', label: 'Armure standard' }) : undefined, () => character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', label: 'Armure standard', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
() => character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', label: 'Armure lourde' }) : undefined, () => character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', label: 'Armure lourde', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
]) : undefined, ]) : undefined,
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [ div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', text(() => character.spellranks.precision)) ]), () => character.spellranks.precision > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Précision') ], { href: 'regles/la-magie/magie#Les sorts de précision', label: 'Précision', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.precision)) ]) : undefined,
div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', text(() => character.spellranks.knowledge)) ]), () => character.spellranks.knowledge > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Savoir') ], { href: 'regles/la-magie/magie#Les sorts de savoir', label: 'Savoir', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.knowledge)) ]) : undefined,
div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', text(() => character.spellranks.instinct)) ]), () => character.spellranks.instinct > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Instinct') ], { href: 'regles/la-magie/magie#Les sorts instinctif', label: 'Instinct', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.instinct)) ]) : undefined,
div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', text(() => character.spellranks.arts)) ]), () => character.spellranks.arts > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Oeuvres') ], { href: 'regles/annexes/œuvres', label: 'Oeuvres', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.arts)) ]) : undefined,
]) ])
]) ])
]), ]),
@ -1636,14 +1656,14 @@ export class CharacterSheet
div('flex flex-col gap-8', [ div('flex flex-col gap-8', [
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]), div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(3).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]), div('flex flex-row items-center gap-2', [ div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]),
]), ]),
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => proses('a', a, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60' }))), div('flex flex-row flex-wrap gap-2', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
div('flex flex-col gap-2', { render: (e) => div('flex flex-col gap-1', [ div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]), div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }), markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.action }), ]), list: character.lists.action }),
@ -1651,14 +1671,14 @@ export class CharacterSheet
]), ]),
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]), div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(2).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]), div('flex flex-row items-center gap-2', [ div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]),
]), ]),
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
div('flex flex-col gap-2', { render: (e) => div('flex flex-col gap-1', [ div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]), div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }), markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.reaction }), ]), list: character.lists.reaction }),
@ -1666,13 +1686,13 @@ export class CharacterSheet
]), ]),
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]), div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"), div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
]), ]),
div('flex flex-col gap-2', [ div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))), div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
div('flex flex-col gap-2', { render: e => div('flex flex-col gap-1', [ div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]), div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]),
markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }), markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.reaction }) ]), list: character.lists.reaction })
@ -1684,7 +1704,7 @@ export class CharacterSheet
abilitiesTab(character: CompiledCharacter) abilitiesTab(character: CompiledCharacter)
{ {
return [ return [
div('flex flex-col gap-2', { render: e => div('flex flex-col gap-1', [ div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]), div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]),
markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }), markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.passive }), ]), list: character.lists.passive }),
@ -1693,36 +1713,57 @@ export class CharacterSheet
spellTab(character: CompiledCharacter) spellTab(character: CompiledCharacter)
{ {
const preference = reactive({ const preference = reactive({
sort: localStorage.getItem('character-sort') ?? 'rank', sort: localStorage.getItem('character-sort') ?? 'rank-asc',
} as { sort: 'rank' | 'type' | 'element' }); } as { sort: `${'rank'|'type'|'element'|'cost'|'range'|'speed'}-${'asc'|'desc'}` | '' });
const sort = (spells: string[]) => { const sort = (spells: string[]) => {
localStorage.setItem('character-sort', preference.sort); localStorage.setItem('character-sort', preference.sort);
const _spells = Object.keys(config.spells);
spells.sort((a, b) => _spells.indexOf(a) - _spells.indexOf(b));
switch(preference.sort) switch(preference.sort)
{ {
case 'rank': return spells.sort((a, b) => ((config.spells[a] as SpellConfig)?.rank ?? 0) - ((config.spells[b] as SpellConfig)?.rank ?? 0) || SPELL_ELEMENTS.indexOf((config.spells[a] as SpellConfig)?.elements[0]!) - SPELL_ELEMENTS.indexOf((config.spells[b] as SpellConfig)?.elements[0]!)); case 'rank-asc': return spells.sort((a, b) => (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
case 'type': return spells.sort((a, b) => (config.spells[a] as SpellConfig)?.type.localeCompare((config.spells[b] as SpellConfig)?.type ?? '') || ((config.spells[a] as SpellConfig)?.rank ?? 0) - ((config.spells[b] as SpellConfig)?.rank ?? 0)); case 'type-asc': return spells.sort((a, b) => config.spells[a]?.type.localeCompare(config.spells[b]?.type ?? '') ?? 0);
case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf((config.spells[a] as SpellConfig)?.elements[0]!) - SPELL_ELEMENTS.indexOf((config.spells[b] as SpellConfig)?.elements[0]!) || ((config.spells[a] as SpellConfig)?.rank ?? 0) - ((config.spells[b] as SpellConfig)?.rank ?? 0)); case 'element-asc': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!));
case 'cost-asc': return spells.sort((a, b) => (config.spells[a]?.cost ?? 0) - (config.spells[b]?.cost ?? 0));
case 'range-asc': return spells.sort((a, b) => (config.spells[a]?.range === 'personnal' ? -1 : config.spells[a]?.range ?? 0) - (config.spells[b]?.range === 'personnal' ? -1 : config.spells[b]?.range ?? 0));
case 'speed-asc': return spells.sort((a, b) => (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0) - (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0));
case 'rank-desc': return spells.sort((b, a) => (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
case 'type-desc': return spells.sort((b, a) => config.spells[a]?.type.localeCompare(config.spells[b]?.type ?? '') ?? 0);
case 'element-desc': return spells.sort((b, a) => SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!));
case 'cost-desc': return spells.sort((b, a) => (config.spells[a]?.cost ?? 0) - (config.spells[b]?.cost ?? 0));
case 'range-desc': return spells.sort((b, a) => (config.spells[a]?.range === 'personnal' ? -1 : config.spells[a]?.range ?? 0) - (config.spells[b]?.range === 'personnal' ? -1 : config.spells[b]?.range ?? 0));
case 'speed-desc': return spells.sort((b, a) => (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0) - (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0));
default: return spells; default: return spells;
} }
}; };
const panel = this.spellPanel(character); const panel = this.spellPanel(character);
const sorter = function(this: HTMLElement) { return followermenu(this, [
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'rank-asc' ? 'rank-desc' : preference.sort === 'rank-desc' ? '' : 'rank-asc') } }, [text('Rang'), () => preference.sort.startsWith('rank') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'type-asc' ? 'type-desc' : preference.sort === 'type-desc' ? '' : 'type-asc') } }, [text('Type'), () => preference.sort.startsWith('type') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'element-asc' ? 'element-desc' : preference.sort === 'element-desc' ? '' : 'element-asc') } }, [text('Element'), () => preference.sort.startsWith('element') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'cost-asc' ? 'cost-desc' : preference.sort === 'cost-desc' ? '' : 'cost-asc') } }, [text('Coût'), () => preference.sort.startsWith('cost') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'range-asc' ? 'range-desc' : preference.sort === 'range-desc' ? '' : 'range-asc') } }, [text('Portée'), () => preference.sort.startsWith('range') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'speed-asc' ? 'speed-desc' : preference.sort === 'speed-desc' ? '' : 'speed-asc') } }, [text('Incantation'), () => preference.sort.startsWith('speed') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
], { class: 'text-light-100 dark:text-dark-100 w-32', offset: 8, placement: 'bottom-end', arrow: true });
}
return [ return [
div('flex flex-col gap-2', [ div('flex flex-col gap-2 h-full', [
div('flex flex-row justify-between items-center', [ div('flex flex-row justify-end items-center', [
div('flex flex-row gap-2 items-center', [
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: preference.sort as 'rank' | 'type' | 'element', class: { option: 'px-2 py-1 text-sm' }, onChange: (v) => { preference.sort = v; } }),
]),
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'), () => panel.show(), 'py-1 px-4'), button(text('Modifier'), () => panel.show(), 'py-1 px-4'),
tooltip(button(icon('ph:arrows-down-up', { width: 16, height: 16 }), sorter, 'p-1'), 'Trier par', 'right')
]) ])
]), ]),
div('flex flex-col gap-2', { render: e => { div('flex flex-col gap-2 overflow-auto', { render: (e, _c) => {
if(_c) return _c;
const spell = config.spells[e] as SpellConfig | undefined; const spell = config.spells[e] as SpellConfig | undefined;
if(!spell) if(!spell)
@ -1742,23 +1783,47 @@ export class CharacterSheet
} }
spellPanel(character: CompiledCharacter) spellPanel(character: CompiledCharacter)
{ {
const availableSpells = Object.values(config.spells).filter(spell => {
if(spell.type === 'arts') return false;
if(spell.rank === 4) return false;
if(character.spellranks[spell.type] < spell.rank) return false;
return true;
}) as SpellConfig[];
const spells = character.variables.spells; const spells = character.variables.spells;
const filters = reactive<{ tag: Array<string>, rank: Array<SpellConfig['rank']>, type: Array<SpellConfig['type']>, element: Array<SpellConfig['elements'][number]>, cost: { min: number, max: number }, range: Array<SpellConfig['range']>, speed: Array<SpellConfig['speed']> }>({
tag: [],
type: [],
rank: [],
element: [],
cost: { min: 0, max: Infinity },
range: [],
speed: [],
});
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-1/2 flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [ 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-1/2 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 mb-4", [ div("flex flex-row justify-between items-center", [
dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }), dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }),
div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ text(() => `${spells.length + (character.lists.spells?.length ?? 0)}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', spells.length + (character.lists.spells?.length ?? 0) > 1 ? 's' : '')) ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => { div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ text(() => `${spells.length + (character.lists.spells?.length ?? 0)}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', spells.length + (character.lists.spells?.length ?? 0) > 1 ? 's' : '')) ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150); setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive'); container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left") ]) }, "p-1"), "Fermer", "left") ])
]), ]),
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', { list: availableSpells, render: (spell) => foldable(() => [ div('flex flex-row gap-2', [
div('flex flex-col gap-1 items-center', [ text('Tags'), multiselect<typeof filters.tag[number]>([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { defaultValue: filters.tag, change: v => filters.tag = v, class: { container: 'w-32 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
div('flex flex-col gap-1 items-center', [ text('Types'), multiselect<typeof filters.type[number]>(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { defaultValue: filters.type, change: v => filters.type = v, class: { container: 'w-28 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
div('flex flex-col gap-1 items-center', [ text('Rangs'), multiselect<typeof filters.rank[number]>([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }], { defaultValue: filters.rank, change: v => filters.rank = v, class: { container: 'w-24 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
div('flex flex-col gap-1 items-center', [ text('Elements'), multiselect<typeof filters.element[number]>(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { defaultValue: filters.element, change: v => filters.element = v, class: { container: 'w-28 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
div('flex flex-col gap-1 items-center', [ text('Portée'), multiselect<typeof filters.range[number]>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { defaultValue: filters.range, change: v => filters.range = v, class: { container: 'w-28 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
div('flex flex-col gap-1 items-center', [ text('Incantation'), multiselect<typeof filters.speed[number]>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { defaultValue: filters.speed, change: v => filters.speed = v, class: { container: 'w-32 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
]),
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', { list: () => Object.values(config.spells).filter(spell => {
//if(spells.includes(spell.id)) return true;
if(character.spellranks[spell.type] < spell.rank) return false;
if(filters.cost.min > spell.cost || spell.cost > filters.cost.max) return false;
if(filters.element.length > 0 && !filters.element.some(e => spell.elements.includes(e))) return false;
if(filters.range.length > 0 && !filters.range.includes(spell.range)) return false;
if(filters.rank.length > 0 && !filters.rank.includes(spell.rank)) return false;
if(filters.type.length > 0 && !filters.type.includes(spell.type)) return false;
if(filters.speed.length > 0 && !filters.speed.includes(spell.speed)) return false;
if(filters.tag.length > 0 && !filters.tag.some(e => spell.tags?.includes(e))) return false;
return true;
}) as SpellConfig[], render: (spell, _c) => _c ?? foldable(() => [
markdown(spell.description), markdown(spell.description),
], [ div("flex flex-row justify-between gap-2", [ ], [ div("flex flex-row justify-between gap-2", [
dom("span", { class: "text-lg font-bold", text: spell.name }), dom("span", { class: "text-lg font-bold", text: spell.name }),
@ -1826,7 +1891,9 @@ export class CharacterSheet
button(text('Modifier'), () => panel.show(), '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, _c) => {
if(_c) return _c;
const item = config.items[e.id]; const item = config.items[e.id];
if(!item) return; if(!item) return;
@ -1837,6 +1904,7 @@ export class CharacterSheet
const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]); const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]);
return foldable(() => [ return foldable(() => [
markdown(getText(item.description)), markdown(getText(item.description)),
() => e.enchantments && e.enchantments.length === 0 ? undefined : div('flex flex-row gap-1', { list: () => e.enchantments!.map(e => config.enchantments[e]).filter(e => !!e), render: (e, _c) => _c ?? floater(div('flex flex-row gap-2 border border-light-35 dark:border-dark-35 bg-light-15 dark:bg-dark-15 px-2 rounded-full py-px bg-light-cyan dark:bg-dark-cyan bg-opacity-20 dark:bg-opacity-20', [ span('text-sm font-semibold tracking-thigh', e.name), div('flex flex-row gap-1 items-center', [icon('game-icons:bolt-drop', { width: 12, height: 12 }), span('text-sm font-light', e.power)]) ]), () => [markdown(getText(e.description), undefined, { tags: { a: preview } })], { class: 'max-w-96 max-h-48 p-2', position: "right" }) }),
div('flex flex-row justify-center gap-1', [ div('flex flex-row justify-center gap-1', [
this.character?.character.campaign ? button(text('Partager'), () => { this.character?.character.campaign ? button(text('Partager'), () => {
@ -1860,20 +1928,20 @@ export class CharacterSheet
this.character?.saveVariables(); this.character?.saveVariables();
}, 'p-1'), }, 'p-1'),
button(text("Enchanter"), () => { () => !item.capacity ? undefined : button(text("Enchanter"), () => {
enchant.show(e); enchant.show(e);
}, 'px-2 text-sm h-5 box-content'), }, 'px-2 text-sm h-5 box-content'),
]) ], [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?.update(e);
//TODO: Toggle effect and enchants
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))) ]),
item.category === 'armor' ? 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', () => `${item.health + ((e.state as ArmorState)?.health ?? 0) - ((e.state as ArmorState)?.loss ?? 0)}/${item.health + ((e.state as ArmorState)?.health ?? 0)} (${[item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0) > 0 ? '-' + (item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0)) : undefined, item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0) > 0 ? '-' + (item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0)) + '%' : undefined].filter(e => !!e).join('/')})`) ]) :
item.category === 'weapon' ? div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:broadsword', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => `${item.damage.value} ${(e.state as WeaponState)?.attack ? '+' + (e.state as WeaponState).attack : ''}`), proses('a', preview, [ text(damageTypeTexts[item.damage.type].toLowerCase()) ], { href: `regles/le-combat/les-types-de-degats#${damageTypeTexts[item.damage.type]}`, label: damageTypeTexts[item.damage.type], navigate: false }) ]) :
undefined
]), ]),
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [ div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price, e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price,
@ -1916,7 +1984,8 @@ export class CharacterSheet
(filters.category.length === 0 || filters.category.includes(item.category)) && (filters.category.length === 0 || filters.category.includes(item.category)) &&
(filters.rarity.length === 0 || filters.rarity.includes(item.rarity)) && (filters.rarity.length === 0 || filters.rarity.includes(item.rarity)) &&
(filters.name === '' || item.name.toLowerCase().includes(filters.name.toLowerCase())) (filters.name === '' || item.name.toLowerCase().includes(filters.name.toLowerCase()))
), render: (e) => { ), render: (e, _c) => {
if(_c) return _c;
const item = config.items[e.id]; const item = config.items[e.id];
if(!item) if(!item)
@ -1988,7 +2057,7 @@ export class CharacterSheet
}, "p-1"), "Fermer", "left") }, "p-1"), "Fermer", "left")
]) ])
]), ]),
div('grid grid-cols-1 -my-2 overflow-y-auto gap-1', { list: () => Object.values(config.enchantments).filter(e => restrict(e, current.item?.id)), render: (enchant) => foldable(() => [ markdown(getText(enchant.description)) ], [div('flex flex-row justify-between', [ div('grid grid-cols-1 -my-2 overflow-y-auto gap-1', { list: () => Object.values(config.enchantments).filter(e => restrict(e, current.item?.id)), render: (enchant, _c) => _c ?? foldable(() => [ markdown(getText(enchant.description)) ], [div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4', [ span('text-lg', enchant.name) ]), div('flex flex-row items-center gap-4', [ span('text-lg', enchant.name) ]),
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [ div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
span('italic text-sm', `Puissance magique: ${enchant.power}`), span('italic text-sm', `Puissance magique: ${enchant.power}`),

View File

@ -696,7 +696,7 @@ export function floater(container: RedrawableHTML, content: NodeChildren | (() =
}); });
minimized = false; minimized = false;
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined, } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ]) div(['group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', settings?.class], typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
], ],
viewport, viewport,
events: events events: events

View File

@ -41,8 +41,9 @@ function append(dom: Element, children: Node | Node[] | undefined)
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation(); export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T]; export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T];
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }; export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> };
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> } export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
{ {
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> }; const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
const _cache = new Map<U, Node | Node[] | undefined>(); const _cache = new Map<U, Node | Node[] | undefined>();
@ -51,11 +52,14 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
{ {
if(Array.isArray(children)) if(Array.isArray(children))
{ {
for(const c of children) reactivity(children, (_children) => {
element.replaceChildren();
for(const c of _children)
{ {
const child = typeof c === 'function' ? c() : c; const child = typeof c === 'function' ? c() : c;
child && element.appendChild(child); child && element.appendChild(child);
} }
});
} }
else if(children.list !== undefined) else if(children.list !== undefined)
{ {
@ -80,12 +84,8 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
else else
{ {
list.forEach(e => { list.forEach(e => {
let child = _cache.get(e); const child = raw(children.render(e, _cache.get(e)));
if(!child)
{
child = raw(children.render(e));
_cache.set(e, child); _cache.set(e, child);
}
append(element, child); append(element, child);
}); });
} }
@ -144,10 +144,9 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
return element; return element;
} }
export function div(cls?: Reactive<Class>, children?: NodeChildren): HTMLElementTagNameMap['div'] export function div(cls?: Reactive<Class>, children?: NodeChildren): HTMLElementTagNameMap['div']
export function div<U extends any>(cls?: Reactive<Class>, children?: { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array: DOMList<U> } export function div<U extends any>(cls?: Reactive<Class>, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array: DOMList<U> }
export function div<U extends any>(cls?: Reactive<Class>, children?: NodeChildren | { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array?: DOMList<U> } export function div<U extends any>(cls?: Reactive<Class>, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array?: DOMList<U> }
{ {
//@ts-expect-error wtf is wrong here ???
return dom("div", { class: cls }, children); return dom("div", { class: cls }, children);
} }
export function span(cls?: Reactive<Class>, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span'] export function span(cls?: Reactive<Class>, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span']

View File

@ -1,4 +1,4 @@
import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponType } from "~/types/character"; import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponType } from "~/types/character";
import { div, dom, icon, span, text, type NodeChildren, type RedrawableHTML } from "#shared/dom.util"; import { div, dom, icon, span, text, type NodeChildren, type RedrawableHTML } from "#shared/dom.util";
import { MarkdownEditor } from "#shared/editor.util"; import { MarkdownEditor } from "#shared/editor.util";
import { preview } from "#shared/proses"; import { preview } from "#shared/proses";
@ -10,6 +10,7 @@ import { getID } from "#shared/general.util";
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util"; import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
import { Tree } from "#shared/tree"; import { Tree } from "#shared/tree";
import { getText, setText } from "#shared/i18n"; import { getText, setText } from "#shared/i18n";
import { reactive } from "./reactive";
type Category = ItemConfig['category']; type Category = ItemConfig['category'];
type Rarity = ItemConfig['rarity']; type Rarity = ItemConfig['rarity'];
@ -211,18 +212,64 @@ export class HomebrewBuilder
} }
spells() spells()
{ {
const spellTagTexts = {
'damage': 'Dégâts',
'buff': 'Buff',
'debuff': 'Débuff',
'support': 'Support',
'tank': 'Tank',
'movement': 'Mouvement',
'utilitary': 'Utilitaire',
} as Record<string, string>;
const editing = reactive({
id: '',
});
const render = (spell: SpellConfig) => { const render = (spell: SpellConfig) => {
return spell.type === 'arts' ? undefined : foldable([ return foldable(() => [
markdown(spell.description, undefined, { tags: { a: preview } }),
], [
div('gap-4 px-4 flex', [
input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }),
div('flex flex-1 flex-row gap-2 items-center', [
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`Rang ${spell.rank}`) ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spellTypeTexts[spell.type]) ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${spell.cost} mana`) ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.speed === 'action' ? 'Action' : spell.speed === 'reaction' ? 'Réaction' : `${spell.speed} minutes`) ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${elementTexts[spell.elements[0]!].text}${spell.elements.length > 1 ? `+${spell.elements.length - 1}` : ''}`) ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.range === 'personnal' ? 'Personnel' : spell.range === 0 ? 'Toucher' : `${spell.range} cases`) ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${spell.tags && spell.tags.length > 0 ? spellTagTexts[spell.tags[0]!] : ''}${spell.tags && spell.tags.length > 1 ? `+${spell.tags.length - 1}` : ''}`) ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.concentration ? 'Concentration' : '') ]),
]),
div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:pencil-1'), () => editing.id = spell.id, 'p-1'), button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ])
])
], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
}
const edit = (spell: SpellConfig) => {
MarkdownEditor.singleton.onChange = v => {};
MarkdownEditor.singleton.content = spell.description;
return foldable([
MarkdownEditor.singleton.dom
], [
div('gap-4 px-4 flex', [
input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }),
div('flex flex-row gap-2 items-center', [
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.filter(e => e !== 'arts').map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Portée'), select<'personnal' | number>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { change: (value) => spell.range = value, defaultValue: spell.range, class: { container: '!m-0 !h-9 w-full' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Portée'), select<'personnal' | number>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { change: (value) => spell.range = value, defaultValue: spell.range, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]),
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]), dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]),
], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }), input('text', { input: (value) => { spell.description = value }, defaultValue: spell.description, class: '!m-0 w-full' }),div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false }); ]),
} div('flex flex-row gap-2', [ ])
])
], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
};
const add = () => { const add = () => {
const id = getID(); const id = getID();
config.spells[id] = { config.spells[id] = {
@ -238,26 +285,16 @@ export class HomebrewBuilder
range: 0, range: 0,
tags: [], tags: [],
}; };
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
}; };
const remove = (spell: SpellConfig) => { const remove = (spell: SpellConfig) => {
confirm('Voulez vous vraiment supprimer ce sort ?').then(e => { confirm('Voulez vous vraiment supprimer ce sort ?').then(e => {
if(e) if(e)
{ {
delete config.spells[spell.id]; delete config.spells[spell.id];
const element = redraw();
content.parentElement?.replaceChild(element, content);
content = element;
} }
}); });
} }
const redraw = () => div('flex flex-col divide-y', Object.values(config.spells).map(render)); return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), div('flex flex-col divide-y', { list: Object.values(config.spells), render: (e, _c) => editing.id === e.id ? edit(e) : _c ?? render(e)}) ] ) ];
let content = redraw();
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
} }
actions() actions()
{ {
@ -571,7 +608,7 @@ class FeatureEditor
} }
else else
{ {
list = (Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add') as FeatureList[]).map((e) => config[e.list][e.item]!).map((e) => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id })); list = (Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add') as FeatureList[]).map((e) => ({ text: config[e.list][e.item]!.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: config[e.list][e.item]!.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(config[e.list === 'sickness' ? 'features' : e.list][e.item]?.description))) ]) ]), value: e.item }));
} }
return { return {
@ -868,7 +905,7 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
{ text: 'Résistance > Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }] } { text: 'Résistance > Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }] }
]} as Partial<FeatureChoice>} ]} as Partial<FeatureChoice>}
] }, ] },
{ text: 'Bonus à l\'attaque', value: RESISTANCES.map(e => ({ text: `Bonus > ${resistanceTexts[e]}`, value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) }, { text: 'Bonus à l\'attaque', value: RESISTANCES.map(e => ({ text: `Bonus > ${resistanceTexts[e]}`, value: { category: 'value', property: `bonus/resistance/${e}`, operation: 'add', value: 1 } })) },
{ text: 'Magie', value: [ { text: 'Magie', value: [
{ text: 'Rang', value: [ { text: 'Rang', value: [
{ text: 'Rang > Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } }, { text: 'Rang > Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } },

View File

@ -43,8 +43,9 @@ export interface MDProperties
class?: Class; class?: Class;
style?: string | Record<string, string>; style?: string | Record<string, string>;
tags?: Record<string, Prose>; tags?: Record<string, Prose>;
includeLowers?: boolean;
} }
export function filterMarkdown(data: Root, filter: string) export function filterMarkdown(data: Root, filter: string, includeLowers: boolean = true)
{ {
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1; const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
@ -55,7 +56,7 @@ export function filterMarkdown(data: Root, filter: string)
while(end < data.children.length) while(end < data.children.length)
{ {
end++; end++;
if(heading(data.children[end]) && headingRank(data.children[end]!)! <= rank) if(heading(data.children[end]) && (!includeLowers || headingRank(data.children[end]!)! <= rank))
break; break;
} }
return { ...data, children: data.children.slice(start, end) }; return { ...data, children: data.children.slice(start, end) };
@ -66,7 +67,7 @@ export function filterMarkdown(data: Root, filter: string)
export function markdownReference(content: string, filter?: string, properties?: MDProperties) export function markdownReference(content: string, filter?: string, properties?: MDProperties)
{ {
const state = async('large', useMarkdown().parse(content).then(data => { const state = async('large', useMarkdown().parse(content).then(data => {
if(filter) data = filterMarkdown(data, filter); if(filter) data = filterMarkdown(data, filter, properties?.includeLowers);
const el = dom('div', properties, data.children.map(e => renderContent(e, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags)))); const el = dom('div', properties, data.children.map(e => renderContent(e, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags))));

View File

@ -37,7 +37,7 @@ export const a: Prose = {
return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => { return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown') if(_content?.type === 'markdown')
{ {
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6', includeLowers: properties?.lowers });
} }
if(_content?.type === 'canvas') if(_content?.type === 'canvas')
{ {
@ -46,7 +46,7 @@ export const a: Prose = {
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]); return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
} }
return div(''); return div('');
})).current], { events: { show: properties?.trigger !== 'click' ? ['mouseenter', 'mousemove', 'focus'] : ['click'], hide: properties?.trigger !== 'click' ? ['mouseleave', 'blur'] : ['click'] }, position: 'bottom-start', pinned: false, title: properties?.label, href: nav.href }) : element; })).current], { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', cover: 'all', events: { show: properties?.trigger !== 'click' ? ['mouseenter', 'mousemove', 'focus'] : ['click'], hide: properties?.trigger !== 'click' ? ['mouseleave', 'blur'] : ['click'] }, position: 'bottom-start', pinned: false, title: properties?.label, href: nav.href }) : element;
} }
} }
export const preview: Prose = { export const preview: Prose = {
@ -65,7 +65,7 @@ export const preview: Prose = {
return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => { return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown') if(_content?.type === 'markdown')
{ {
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6', includeLowers: false });
} }
if(_content?.type === 'canvas') if(_content?.type === 'canvas')
{ {
@ -74,11 +74,8 @@ export const preview: Prose = {
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]); return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
} }
return div(); return div();
})).current], { position: 'bottom-start', pinned: true, })).current], { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', delay: 500, cover: 'all', events: { show: properties?.trigger !== 'click' ? ['mouseenter', 'mousemove', 'focus'] : ['click'], hide: properties?.trigger !== 'click' ? ['mouseleave', 'blur'] : ['click'] }, position: 'bottom-start', pinned: false, title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
events: { //})).current], { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', position: 'bottom-start', pinned: false, delay: 0, cover: 'all', title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
show: ['click'],
hide: ['click'],
}, title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
} }
} }
export const callout: Prose = { export const callout: Prose = {

View File

@ -153,7 +153,7 @@ const arraySubstitute = <any>{ // <-- <any> is required to allow __proto__ witho
toSorted(comparer?: (a: unknown, b: unknown) => number) { return reactiveReadArray(this).toSorted(comparer) }, toSorted(comparer?: (a: unknown, b: unknown) => number) { return reactiveReadArray(this).toSorted(comparer) },
toSpliced(...args: unknown[]) { return (reactiveReadArray(this).toSpliced as any)(...args) }, toSpliced(...args: unknown[]) { return (reactiveReadArray(this).toSpliced as any)(...args) },
unshift(...args: unknown[]) { return noTracking(this, 'unshift', args) }, unshift(...args: unknown[]) { return noTracking(this, 'unshift', args) },
values() { return iterator(this, 'values', item => wrapReactive(item)) }, /* */ values() { return iterator(this, 'values', item => wrapReactive(item)) },
}; };
// Store object to proxy correspondance // Store object to proxy correspondance
@ -228,9 +228,6 @@ export function reactive<T extends object>(obj: T | Proxy<T>): T | Proxy<T>
if(_reactiveCache.has(obj)) if(_reactiveCache.has(obj))
return _reactiveCache.get(obj)!; return _reactiveCache.get(obj)!;
const prototype = Object.getPrototypeOf(obj);
const isArray = Array.isArray(obj);
const proxy = new Proxy<T>(obj, { const proxy = new Proxy<T>(obj, {
get: (target, key, receiver) => { get: (target, key, receiver) => {
if(key === SYMBOLS.PROXY) if(key === SYMBOLS.PROXY)