Fix dynamic character sheet loading.

This commit is contained in:
Clément Pons
2026-03-09 17:27:18 +01:00
parent 974989abd3
commit 3bafc14255
10 changed files with 329 additions and 14505 deletions

View File

@@ -92,8 +92,10 @@ onMounted(async () => {
tree.value.appendChild(load);
const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [
tooltip(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), 'Actualiser', 'top'),
tooltip(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), 'Enregistrer', 'top'),
tooltip(button(icon('ph:cloud-arrow-down', { height: 16, width: 16 }), pull, 'p-1'), 'Actualiser', 'top'),
tooltip(button(icon('ph:cloud-arrow-up', { height: 16, width: 16 }), push, 'p-1'), 'Enregistrer', 'top'),
tooltip(button(icon('radix-icons:reset', { height: 16, width: 16 }), () => editor?.undo(), 'p-1'), 'Annuler', 'top'),
tooltip(button(icon('radix-icons:reset', { height: 16, width: 16, hFlip: true }), () => editor?.redo(), 'p-1'), 'Rétablir', 'top'),
])
tree.value.insertBefore(content, load);
@@ -105,6 +107,11 @@ onMounted(async () => {
}
});
useShortcuts({
"Meta-Z": () => editor?.undo(),
"Meta-Y": () => editor?.redo(),
});
onBeforeUnmount(() => {
editor?.unmount();
});

View File

@@ -129,7 +129,7 @@ type CommonItemConfig = {
powercost?: number; //Optionnal
charge?: number //Max amount of charges
enchantments?: string[]; //Enchantment ID
effects?: Array<FeatureValue | FeatureEquipment | FeatureList>;
effects?: Array<FeatureValue | FeatureState | FeatureEquipment | FeatureList>;
equippable: boolean;
consummable: boolean;
craft?: { mineral: number, natural: number, processed: number, magical: number };
@@ -168,15 +168,6 @@ export type SpellConfig = {
range: 'personnal' | number;
tags?: string[];
};
export type ArtConfig = {
id: string;
name: string; //TODO -> TextID
rank: 1 | 2 | 3;
type: "arts";
difficulty: number;
description: string; //TODO -> TextID
tags?: string[];
};
export type RaceConfig = {
id: string;
name: string; //TODO -> TextID
@@ -204,6 +195,12 @@ export type FeatureValue = {
property: RecursiveKeyOf<CompiledCharacter> | 'spec' | 'ability' | 'training';
value: number | `modifier/${MainStat}` | false;
}
export type FeatureState = {
id: FeatureEffectID;
category: "state";
property: RecursiveKeyOf<CompiledCharacter>;
value: string;
}
export type FeatureEquipment = {
id: FeatureEffectID;
category: "value";
@@ -235,7 +232,7 @@ export type FeatureChoice = {
};
options: Array<{ text: string, effects: Array<FeatureValue | FeatureList | FeatureTree> }>; //TODO -> TextID
};
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice | FeatureTree;
export type FeatureItem = FeatureValue | FeatureState | FeatureList | FeatureChoice | FeatureTree;
export type Feature = {
id: FeatureID;
description: string; //TODO -> TextID
@@ -252,7 +249,7 @@ export type CompiledCharacter = {
race: string;
spellslots: number; //Max
artslots: number; //Max
spellranks: Record<SpellType | 'arts', 0 | 1 | 2 | 3>;
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;
aspect: {
id: string,
amount: number;
@@ -290,6 +287,7 @@ export type CompiledCharacter = {
};
weapon: Partial<Record<WeaponType, number>>;
resistance: Partial<Record<Resistance, number>>; //Bonus à l'attaque
damage: Partial<DamageType, 'resistance' | 'immunity' | 'vulnerability'>;
}; //Any special bonus goes here
craft: { level: number, bonus: number };

View File

@@ -14,7 +14,7 @@ export default defineEventHandler(async (e) => {
try
{
const id = getRouterParam(e, "id") ?? '';
const body = await readRawBody(e);
const body = Buffer.from(await readRawBody(e) ?? '');
if(!id)
{
@@ -32,7 +32,7 @@ export default defineEventHandler(async (e) => {
}
db.insert(projectContentTable).values({ id: id, content: body }).onConflictDoUpdate({ set: { content: body }, target: projectContentTable.id }).run();
db.update(projectFilesTable).set({ timestamp: new Date() }).where(eq(projectFilesTable.id, id)).run()
db.update(projectFilesTable).set({ timestamp: new Date() }).where(eq(projectFilesTable.id, id)).run();
}
catch(_e)
{

View File

@@ -1,7 +1,7 @@
import useDatabase from '~/composables/useDatabase';
import { hasPermissions } from "#shared/auth";
import { eq, sql } from "drizzle-orm";
import { projectFilesTable } from "~/db/schema";
import { projectContentTable, projectFilesTable } from "~/db/schema";
import { Project } from "~/schemas/project";
export default defineEventHandler(async (e) => {
@@ -19,23 +19,24 @@ export default defineEventHandler(async (e) => {
throw body.error;
}
const db = useDatabase(), items = body.data, blocked: string[] = [];
const db = useDatabase(), items = body.data, requested: string[] = [];
db.transaction((tx) => {
const data = tx.select({ id: projectFilesTable.id, timestamp: projectFilesTable.timestamp }).from(projectFilesTable).all();
const deletion = tx.delete(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare();
const deleteOverview = tx.delete(projectFilesTable).where(eq(projectFilesTable.id, sql.placeholder('id'))).prepare();
const deleteContent = tx.delete(projectContentTable).where(eq(projectContentTable.id, sql.placeholder('id'))).prepare();
for(let i = 0; i < items.length; i++)
{
const item = items[i];
const submitted = items[i]!;
const index = data.findIndex(e => e.id === item.id);
const index = data.findIndex(e => e.id === submitted.id);
if(index !== -1)
{
if(data[index].timestamp > new Date(item.timestamp))
if(data[index]!.timestamp < new Date(submitted.timestamp))
{
blocked.push(item.id);
requested.push(submitted.id);
continue;
}
@@ -43,34 +44,36 @@ export default defineEventHandler(async (e) => {
}
tx.insert(projectFilesTable).values({
id: item.id,
path: item.path,
id: submitted.id,
path: submitted.path,
owner: user.id,
title: item.title,
type: item.type,
navigable: item.navigable,
private: item.private,
order: item.order,
title: submitted.title,
type: submitted.type,
navigable: submitted.navigable,
private: submitted.private,
order: submitted.order,
}).onConflictDoUpdate({
set: {
id: item.id,
path: item.path,
title: item.title,
type: item.type,
navigable: item.navigable,
private: item.private,
order: item.order,
id: submitted.id,
path: submitted.path,
title: submitted.title,
type: submitted.type,
navigable: submitted.navigable,
private: submitted.private,
order: submitted.order,
timestamp: new Date(),
},
target: projectFilesTable.id,
}).run();
}
//Delete the remaining data has they have not been found in the new overview
for(let i = 0; i < data.length; i++)
{
deletion.run({ id: data[i].id });
deleteOverview.run({ id: data[i]!.id });
deleteContent.run({ id: data[i]!.id });
}
});
return blocked;
return requested;
});

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CompiledCharacter, DamageType, EnchantementConfig, FeatureEquipment, FeatureID, FeatureItem, FeatureList, FeatureValue, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, TreeStructure, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CompiledCharacter, DamageType, EnchantementConfig, FeatureEquipment, FeatureID, FeatureItem, FeatureList, FeatureState, FeatureValue, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, TreeStructure, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json';
import proses, { preview } from "#shared/proses";
@@ -98,7 +98,6 @@ const defaultCompiledCharacter = (character: Character) => ({
instinct: 0 as 0 | 1 | 2 | 3,
knowledge: 0 as 0 | 1 | 2 | 3,
precision: 0 as 0 | 1 | 2 | 3,
arts: 0 as 0 | 1 | 2 | 3,
},
speed: false as number | false,
defense: {
@@ -120,6 +119,7 @@ const defaultCompiledCharacter = (character: Character) => ({
},
weapon: {},
resistance: {},
damage: {}
},
initiative: 0,
capacity: 0,
@@ -233,6 +233,11 @@ export const damageTypeTexts: Record<DamageType, string> = {
'slashing': 'Tranchant',
'thunder': 'Foudre',
};
export const protectionTexts: Record<'resistance' | 'immunity' | 'vulnerability', { summary: string, detail: string }> = {
immunity: { summary: 'x0', detail: 'Immunité' },
resistance: { summary: '1/2', detail: 'Résistance' },
vulnerability: { summary: 'x2', detail: 'Vulnérabilité' }
}
export const CharacterNotesValidation = z.object({
public: z.string().optional(),
@@ -290,9 +295,9 @@ export class CharacterCompiler
{
private _dirty: boolean = true;
protected _character: Character | undefined;
protected _result: CompiledCharacter | undefined;
protected _buffer: Record<string, PropertySum> = {
protected _character: Character = reactive(defaultCharacter);
private _result: CompiledCharacter = reactive(defaultCompiledCharacter(defaultCharacter));
private _buffer: Record<string, PropertySum> = {
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/constitution': { value: 0, _dirty: false, min: -Infinity, list: [] },
@@ -301,8 +306,7 @@ export class CharacterCompiler
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
};
protected _trees: Record<string, TreeState> = {};
private _variableDebounce: NodeJS.Timeout = setTimeout(() => {});
private _trees: Record<string, TreeState> = {};
constructor(character?: Character)
{
@@ -311,8 +315,8 @@ export class CharacterCompiler
set character(value: Character)
{
this._character = reactive(value);
this._result = reactive(defaultCompiledCharacter(value));
Object.assign(this._character, {}, value);
Object.assign(this._result, {}, defaultCompiledCharacter(value));
this._buffer = {
'modifier/strength': { value: 0, _dirty: false, min: -Infinity, list: [] },
'modifier/dexterity': { value: 0, _dirty: false, min: -Infinity, list: [] },
@@ -332,6 +336,8 @@ export class CharacterCompiler
Object.entries(value.abilities).forEach(e => this._buffer[`abilities/${e[0]}`] = { value: 0, _dirty: true, min: -Infinity, list: [{ id: '', operation: 'add', value: e[1] }] });
value.variables.items.forEach(e => this.enchant(e));
reactivity(() => value.variables.transformed, (v) => {
if(this._character && this._result && value.aspect && config.aspects[value.aspect])
{
@@ -350,16 +356,15 @@ export class CharacterCompiler
this._buffer[`modifier/${aspect.stat}`]!._dirty = true;
}
this.compile([`modifier/${aspect.stat}`], this._buffer, this._result);
this.saveVariables();
}
})
}
}
get character(): Character | undefined
get character(): Character
{
return this._character;
}
get compiled(): CompiledCharacter | undefined
get compiled(): CompiledCharacter
{
if(this._character && this._result && this._dirty)
{
@@ -410,24 +415,30 @@ export class CharacterCompiler
}
get armor()
{
const armor = this._character?.variables.items.find(e => e.equipped && config.items[e.id]?.category === 'armor');
return armor ? { max: (config.items[armor.id] as ArmorConfig).health, current: (config.items[armor.id] as ArmorConfig).health - ((armor.state as ArmorState)?.loss ?? 0), flat: (config.items[armor.id] as ArmorConfig).absorb.static + ((armor.state as ArmorState)?.absorb.flat ?? 0), percent: (config.items[armor.id] as ArmorConfig).absorb.percent + ((armor.state as ArmorState)?.absorb.percent ?? 0) } : { max: 0, current: 0, flat: 0, percent: 0, };
const armor = this._character.variables.items.find(e => e.equipped && config.items[e.id]?.category === 'armor');
return armor ? { max: (config.items[armor.id] as ArmorConfig).health, current: (config.items[armor.id] as ArmorConfig).health - ((armor.state as ArmorState)?.loss ?? 0), flat: (config.items[armor.id] as ArmorConfig).absorb.static + ((armor.state as ArmorState)?.absorb.flat ?? 0), percent: (config.items[armor.id] as ArmorConfig).absorb.percent + ((armor.state as ArmorState)?.absorb.percent ?? 0), state: armor } : { max: 0, current: 0, flat: 0, percent: 0, state: armor };
}
get weight()
{
return this._character?.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0) ?? 0;
return this._character.variables.items.reduce((p, v) => p + (config.items[v.id]?.weight ?? 0) * v.amount, 0) ?? 0;
}
get power()
{
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) ?? 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) ?? 0;
}
enchant(item: ItemState)
{
if(item.equipped)
item.enchantments?.forEach(e => config.enchantments[e]?.effect.filter(e => e.category !== 'value' || !e.property.startsWith('item')).forEach(_e => this.apply(_e as FeatureValue | FeatureList)));
{
config.items[item.id]?.effects?.filter(e => e.category !== 'value' || !e.property.startsWith('item/'))?.forEach(f => this.apply(f as FeatureValue | FeatureState | FeatureList))
item.enchantments?.forEach(e => config.enchantments[e]?.effect.filter(e => e.category !== 'value' || !e.property.startsWith('item')).forEach(f => this.apply(f as FeatureValue | FeatureState | FeatureList)));
}
else
item.enchantments?.forEach(e => config.enchantments[e]?.effect.filter(e => e.category !== 'value' || !e.property.startsWith('item')).forEach(_e => this.undo(_e as FeatureValue | FeatureList)));
{
config.items[item.id]?.effects?.filter(e => e.category !== 'value' || !e.property.startsWith('item/'))?.forEach(f => this.undo(f as FeatureValue | FeatureState | FeatureList))
item.enchantments?.forEach(e => config.enchantments[e]?.effect.filter(e => e.category !== 'value' || !e.property.startsWith('item')).forEach(f => this.undo(f as FeatureValue | FeatureState | FeatureList)));
}
item.buffer ??= {} as Record<string, PropertySum>;
Object.keys(item.buffer).forEach(e => item.buffer![e]!.list = []);
@@ -441,33 +452,7 @@ export class CharacterCompiler
item.buffer![property]!._dirty = true;
}));
Object.keys(item.buffer).forEach(e => setProperty(item.state, e, 0, true));
this.compile(Object.keys(item.buffer), item.buffer, item.state);
this.saveVariables();
}
saveVariables()
{
if(!this._character) return;
clearTimeout(this._variableDebounce);
this._variableDebounce = setTimeout(() => {
if(!this._character) return;
useRequestFetch()(`/api/character/${this._character.id}/variables`, {
method: 'POST',
body: raw(this._character.variables),
}).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
})
}, 2000);
}
saveNotes()
{
if(!this._character) return Promise.resolve();
return useRequestFetch()(`/api/character/${this._character.id}/notes`, {
method: 'POST',
body: this._character.notes,
}).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
});
this.compile(Object.keys(item.buffer), item.buffer, item.state!);
}
protected add(feature?: FeatureID)
{
@@ -485,8 +470,6 @@ export class CharacterCompiler
}
protected apply(feature?: FeatureItem)
{
if(!this._character) return;
if(!this._result) return;
if(!feature) return;
this._dirty = true;
@@ -502,13 +485,16 @@ export class CharacterCompiler
case "value":
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
if(!this._buffer[feature.property]!.list.find(e => e.id === feature.id))
{
this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
this._buffer[feature.property]!.min = -Infinity;
this._buffer[feature.property]!._dirty = true;
this._buffer[feature.property]!.min = -Infinity;
this._buffer[feature.property]!._dirty = true;
if(feature.property.startsWith('modifier/'))
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
if(feature.property.startsWith('modifier/'))
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
}
return;
case "choice":
@@ -517,6 +503,9 @@ export class CharacterCompiler
if(choice)
choice.forEach(e => feature.options[e]!.effects.forEach((effect) => this.apply(effect)));
return;
case "state":
setProperty(this._result, feature.property, feature.value, true);
return;
case "tree":
if(!config.trees[feature.tree])
@@ -533,8 +522,6 @@ export class CharacterCompiler
}
protected undo(feature?: FeatureItem)
{
if(!this._character) return;
if(!this._result) return;
if(!feature) return;
this._dirty = true;
@@ -566,6 +553,9 @@ export class CharacterCompiler
if(choice)
choice.forEach(e => feature.options[e]!.effects.forEach((effect) => this.undo(effect)));
return;
case "state":
setProperty(this._result, feature.property, undefined);
return;
case "tree":
if(!config.trees[feature.tree] || !this._trees[feature.tree])
@@ -579,7 +569,7 @@ export class CharacterCompiler
return;
}
}
protected compile(queue: string[], _buffer: Record<string, PropertySum> = this._buffer, _result: Record<string, any>)
protected compile(queue: string[], _buffer: Record<string, PropertySum> = this._buffer, _result: Record<string, any> = this._result)
{
for(let i = 0; i < queue.length; i++)
{
@@ -1524,20 +1514,23 @@ export const stateFactory = (item: ItemConfig) => {
}
return state;
}
export class CharacterSheet
export class CharacterSheet extends CharacterCompiler
{
private user: ComputedRef<User | null>;
private compiler: CharacterCompiler = reactive(new CharacterCompiler());
container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-between');
editingDOM: HTMLElement = div();
readingDOM: HTMLElement = div();
private editingDOM: HTMLElement = div();
private readingDOM: HTMLElement = div();
private state: { exists: boolean, edit: boolean };
private tabs?: HTMLElement;
private tab: string = localStorage.getItem('character-tab') ?? 'actions';
private _variableDebounce: NodeJS.Timeout = setTimeout(() => {});
ws?: Socket;
constructor(id: string, user: ComputedRef<User | null>, editing: boolean = false)
{
super(defaultCharacter);
this.user = user;
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
this.container.replaceChildren(load);
@@ -1551,13 +1544,13 @@ export class CharacterSheet
useRequestFetch()(`/api/character/${id}`).then(character => {
if(character)
{
this.compiler.character = reactive(character);
this.character = reactive(character);
if(character.campaign)
{
/* this.ws = new Socket(`/ws/campaign/${character.campaign}`, true);
//this.ws = new Socket(`/ws/campaign/${character.campaign}`, true);
this.ws.handleMessage('SYNC', () => {
/* this.ws.handleMessage('SYNC', () => {
useRequestFetch()(`/api/character/${id}`).then(character => {
if(character)
{
@@ -1587,10 +1580,10 @@ export class CharacterSheet
document.title = `d[any] - ${character.name}`;
load.remove();
this.container.replaceChildren(this.editingDOM, this.readingDOM);
this.container.replaceChildren(this.readingDOM);
}
else
throw new Error();
throw new Error('Cannot find the requested character from its ID');
}).catch((e) => {
console.error(e);
this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
@@ -1599,13 +1592,18 @@ export class CharacterSheet
div('flex flex-row gap-4 justify-center items-center', [
button(text('Personnages publics'), () => useRouter().push({ name: 'character-list' }), 'px-2 py-1'),
button(text('Créer un personange'), () => useRouter().push({ name: 'character-id-edit', params: { id: 'new' } }), 'px-2 py-1')
])
]),
span('text-sm italic text-light-70 dark:text-dark-70', e.toString())
]))
});
}
else
{
this.state.edit = true;
this.container.replaceChildren(this.editingDOM, this.readingDOM);
}
reactivity(this.state, (state) => {
state.edit ? this.container.replaceChildren(this.editingDOM, this.readingDOM) : this.container.replaceChildren(this.readingDOM);
})
@@ -1615,24 +1613,24 @@ export class CharacterSheet
const publicNotes = new MarkdownEditor();
const privateNotes = new MarkdownEditor();
const healthPanel = this.healthPanel(this.compiler.compiled);
const healthPanel = this.healthPanel();
const loadableIcon = icon('radix-icons:paper-plane', { width: 16, height: 16 });
const saveLoading = loading('small');
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.compiler.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
const saveNotes = () => { loadableIcon.replaceWith(saveLoading); this.saveNotes().finally(() => { saveLoading.replaceWith(loadableIcon) }); }
this.tabs = tabgroup([
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(this.compiler.compiled) },
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab() },
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(this.compiler.compiled) },
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab() },
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(this.compiler.compiled) },
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab() },
{ id: 'inventory', title: [ text('Inventaire') ], content: () => this.itemsTab(this.compiler.compiled) },
{ id: 'inventory', title: [ text('Inventaire') ], content: () => this.itemsTab() },
{ id: 'aspect', title: [ span(() => ({ 'relative before:absolute before:top-0 before:-right-2 before:w-2 before:h-2 before:rounded-full before:bg-accent-blue': this.compiler.compiled?.variables?.transformed ?? false }), 'Aspect') ], content: () => this.aspectTab(this.compiler.compiled) },
{ id: 'aspect', title: [ span(() => ({ 'relative before:absolute before:top-0 before:-right-2 before:w-2 before:h-2 before:rounded-full before:bg-accent-blue': this.compiled?.variables?.transformed ?? false }), 'Aspect') ], content: () => this.aspectTab() },
{ id: 'effects', title: [ text('Afflictions') ], content: () => this.effectsTab(this.compiler.compiled) },
{ id: 'effects', title: [ text('Afflictions') ], content: () => this.effectsTab() },
{ id: 'notes', title: [ text('Notes') ], content: () => [
div('flex flex-col h-full divide-y divide-light-30 dark:divide-dark-30', [
@@ -1646,7 +1644,7 @@ export class CharacterSheet
}),
])
] },
], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 max-w-[960px] h-full', content: 'overflow-auto h-full' }, switch: v => { this.tab = v; localStorage.setItem('this.compiler.compiled-tab', v); } });
], { focused: this.tab, class: { container: 'flex-1 gap-4 px-4 max-w-[960px] h-full', content: 'overflow-auto h-full' }, switch: v => { this.tab = v; localStorage.setItem('this.compiled-tab', v); } });
this.readingDOM = div('flex flex-col justify-start gap-1 h-full w-full min-w-half', [
div("flex flex-row gap-4 justify-between", [
@@ -1661,43 +1659,43 @@ export class CharacterSheet
]),
div("flex flex-col", [
span("text-xl font-bold", () => this.compiler.compiled?.name ?? "Inconnu"),
span("text-sm", () => this.compiler.compiled ? `De ${this.compiler.compiled.username}` : '')
span("text-xl font-bold", () => this.compiled.name === '' ? "Inconnu" : this.compiled.name),
span("text-sm", () => this.compiled.username ? `De ${this.compiled.username}` : `De ${this.user.value?.username}`)
]),
div("flex flex-col", [
span("font-bold", () =>`Niveau ${this.compiler.compiled?.level ?? 0}`),
span('', () => this.compiler.compiled && this.compiler.compiled.race ? config.peoples[this.compiler.compiled.race]?.name ?? 'Peuple inconnu' : '')
span("font-bold", () =>`Niveau ${this.compiled?.level ?? 0}`),
span('', () => this.compiled && this.compiled.race ? config.peoples[this.compiled.race]?.name ?? 'Peuple inconnu' : '')
])
]),
div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [
div("flex flex-row items-center gap-2 text-3xl font-light", [
text("PV: "),
() => this.compiler.compiled ? dom("span", {
() => this.compiled ? dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: () => `${this.compiler.compiled.health - this.compiler.compiled.variables.health}`,
text: () => `${this.compiled.health - this.compiled.variables.health}`,
listeners: { click: healthPanel.show },
}) : undefined,
() => this.compiler.compiled ? text('/') : text('-'),
() => this.compiler.compiled ? text(() => this.compiler.compiled.health) : undefined,
() => this.compiled ? text('/') : text('-'),
() => this.compiled ? text(() => this.compiled.health) : undefined,
]),
div("flex flex-row items-center gap-2 text-3xl font-light", [
text("Mana: "),
() => this.compiler.compiled ? dom("span", {
() => this.compiled ? dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: () => `${this.compiler.compiled.mana - this.compiler.compiled.variables.mana}`,
text: () => `${this.compiled.mana - this.compiled.variables.mana}`,
listeners: { click: healthPanel.show },
}) : undefined,
() => this.compiler.compiled ? text('/') : text('-'),
() => this.compiler.compiled ? text(() => this.compiler.compiled.mana) : undefined,
() => this.compiled ? text('/') : text('-'),
() => this.compiled ? text(() => this.compiled.mana) : undefined,
]),
]),
]),
div("self-center", [
this.user.value && this.compiler.compiled && this.user.value.id === this.compiler.compiled.owner ?
() => this.state.edit ? button(icon("ph:floppy-disk"), () => this.saveEdits(), "p-1") : button(icon("radix-icons:pencil-2"), () => this.state.edit = true, "p-1")
() => this.user.value && this.compiled && this.user.value.id === this.compiled.owner ?
this.state.edit ? button(icon("ph:floppy-disk"), () => this.saveEdits(), "p-1") : button(icon("radix-icons:pencil-2"), () => this.state.edit = true, "p-1")
: div(),
]),
]),
@@ -1705,31 +1703,31 @@ export class CharacterSheet
div("flex flex-row justify-center gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
div("flex gap-2 flex-row items-center justify-between", [
div("flex flex-col items-center px-2", [
() => !this.compiler.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiler.compiled.variables.transformed && config.aspects[this.compiler.compiled.aspect.id]?.stat === 'strength' }], () => `+${this.compiler.compiled.modifier.strength}`),
() => !this.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiled.variables.transformed && config.aspects[this.compiled.aspect.id]?.stat === 'strength' }], () => `+${this.compiled.modifier.strength}`),
span("text-sm ", "Force")
]),
div("flex flex-col items-center px-2", [
() => !this.compiler.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiler.compiled.variables.transformed && config.aspects[this.compiler.compiled.aspect.id]?.stat === 'dexterity' }], () => `+${this.compiler.compiled.modifier.dexterity}`),
() => !this.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiled.variables.transformed && config.aspects[this.compiled.aspect.id]?.stat === 'dexterity' }], () => `+${this.compiled.modifier.dexterity}`),
span("text-sm ", "Dextérité")
]),
div("flex flex-col items-center px-2", [
() => !this.compiler.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiler.compiled.variables.transformed && config.aspects[this.compiler.compiled.aspect.id]?.stat === 'constitution' }], () => `+${this.compiler.compiled.modifier.constitution}`),
() => !this.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiled.variables.transformed && config.aspects[this.compiled.aspect.id]?.stat === 'constitution' }], () => `+${this.compiled.modifier.constitution}`),
span("text-sm ", "Constitution")
]),
div("flex flex-col items-center px-2", [
() => !this.compiler.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiler.compiled.variables.transformed && config.aspects[this.compiler.compiled.aspect.id]?.stat === 'intelligence' }], () => `+${this.compiler.compiled.modifier.intelligence}`),
() => !this.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiled.variables.transformed && config.aspects[this.compiled.aspect.id]?.stat === 'intelligence' }], () => `+${this.compiled.modifier.intelligence}`),
span("text-sm ", "Intelligence")
]),
div("flex flex-col items-center px-2", [
() => !this.compiler.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiler.compiled.variables.transformed && config.aspects[this.compiler.compiled.aspect.id]?.stat === 'curiosity' }], () => `+${this.compiler.compiled.modifier.curiosity}`),
() => !this.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiled.variables.transformed && config.aspects[this.compiled.aspect.id]?.stat === 'curiosity' }], () => `+${this.compiled.modifier.curiosity}`),
span("text-sm ", "Curiosité")
]),
div("flex flex-col items-center px-2", [
() => !this.compiler.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiler.compiled.variables.transformed && config.aspects[this.compiler.compiled.aspect.id]?.stat === 'charisma' }], () => `+${this.compiler.compiled.modifier.charisma}`),
() => !this.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiled.variables.transformed && config.aspects[this.compiled.aspect.id]?.stat === 'charisma' }], () => `+${this.compiled.modifier.charisma}`),
span("text-sm ", "Charisme")
]),
div("flex flex-col items-center px-2", [
() => !this.compiler.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiler.compiled.variables.transformed && config.aspects[this.compiler.compiled.aspect.id]?.stat === 'psyche' }], () => `+${this.compiler.compiled.modifier.psyche}`),
() => !this.compiled ? span('text-xl font-bold', '-') : span(() => ["text-xl font-bold", { 'text-accent-blue': this.compiled.variables.transformed && config.aspects[this.compiled.aspect.id]?.stat === 'psyche' }], () => `+${this.compiled.modifier.psyche}`),
span("text-sm ", "Psyché")
])
]),
@@ -1738,11 +1736,11 @@ export class CharacterSheet
div("flex gap-2 flex-row items-center justify-between", [
div("flex flex-col px-2 items-center", [
span("text-xl font-bold", () => !this.compiler.compiled ? '-' : `+${this.compiler.compiled.initiative}`),
span("text-xl font-bold", () => !this.compiled ? '-' : `+${this.compiled.initiative}`),
span("text-sm ", "Initiative")
]),
div("flex flex-col px-2 items-center", [
span("text-xl font-bold", () => !this.compiler.compiled ? '-' : this.compiler.compiled.speed === false ? "N/A" : `${this.compiler.compiled.speed}`),
span("text-xl font-bold", () => !this.compiled ? '-' : this.compiled.speed === false ? "N/A" : `${this.compiled.speed}`),
span("text-sm ", "Course")
])
]),
@@ -1752,15 +1750,15 @@ export class CharacterSheet
div("flex gap-2 flex-row items-center justify-between", [
icon("game-icons:checked-shield", { width: 32, height: 32 }),
div("flex flex-col px-2 items-center", [
span(" text-xl font-bold", () => !this.compiler.compiled ? '-' : clamp(this.compiler.compiled.defense.static + this.compiler.compiled.defense.passiveparry + this.compiler.compiled.defense.passivedodge, 0, this.compiler.compiled.defense.hardcap)),
span(" text-xl font-bold", () => !this.compiled ? '-' : clamp(this.compiled.defense.static + this.compiled.defense.passiveparry + this.compiled.defense.passivedodge, 0, this.compiled.defense.hardcap)),
span("text-sm ", "Passive")
]),
div("flex flex-col px-2 items-center", [
span(" text-xl font-bold", () => !this.compiler.compiled ? '-' : clamp(this.compiler.compiled.defense.static + this.compiler.compiled.defense.activeparry + this.compiler.compiled.defense.passivedodge, 0, this.compiler.compiled.defense.hardcap)),
span(" text-xl font-bold", () => !this.compiled ? '-' : clamp(this.compiled.defense.static + this.compiled.defense.activeparry + this.compiled.defense.passivedodge, 0, this.compiled.defense.hardcap)),
span("text-sm ", "Blocage")
]),
div("flex flex-col px-2 items-center", [
span(" text-xl font-bold", () => !this.compiler.compiled ? '-' : clamp(this.compiler.compiled.defense.static + this.compiler.compiled.defense.passiveparry + this.compiler.compiled.defense.activedodge, 0, this.compiler.compiled.defense.hardcap)),
span(" text-xl font-bold", () => !this.compiled ? '-' : clamp(this.compiled.defense.static + this.compiled.defense.passiveparry + this.compiled.defense.activedodge, 0, this.compiled.defense.hardcap)),
span("text-sm ", "Esquive")
])
]),
@@ -1778,7 +1776,7 @@ export class CharacterSheet
ABILITIES.map((ability) =>
div("flex flex-row px-1 justify-between items-center", [
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", () => !this.compiler.compiled ? '-' : `+${this.compiler.compiled.abilities[ability as Ability] ?? 0}`),
span("font-bold text-base text-light-100 dark:text-dark-100", () => !this.compiled ? '-' : `+${this.compiled.abilities[ability as Ability] ?? 0}`),
])
)
),
@@ -1788,12 +1786,11 @@ export class CharacterSheet
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
]),
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", { list: () => this.compiler.compiled?.mastery ?? [], render: (e, _c) => proses('a', preview, [ text(masteryTexts[e].text) ], { href: masteryTexts[e].href, label: masteryTexts[e].text, class: 'text-sm text-light-70 dark:text-dark-70 cursor-help decoration-dotted underline', }) }),
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", { list: () => this.compiled?.mastery ?? [], render: (e, _c) => proses('a', preview, [ text(masteryTexts[e].text) ], { href: masteryTexts[e].href, label: masteryTexts[e].text, class: 'text-sm text-light-70 dark:text-dark-70 cursor-help decoration-dotted underline', }) }),
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
() => (this.compiler.compiled?.spellranks?.precision ?? 0) > 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 cursor-help decoration-dotted underline' }), span('font-bold', () => this.compiler.compiled?.spellranks?.precision ?? 0) ]) : undefined,
() => (this.compiler.compiled?.spellranks?.knowledge ?? 0) > 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 cursor-help decoration-dotted underline' }), span('font-bold', () => this.compiler.compiled?.spellranks?.knowledge ?? 0) ]) : undefined,
() => (this.compiler.compiled?.spellranks?.instinct ?? 0) > 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 cursor-help decoration-dotted underline' }), span('font-bold', () => this.compiler.compiled?.spellranks?.instinct ?? 0) ]) : undefined,
() => (this.compiler.compiled?.spellranks?.arts ?? 0) > 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 cursor-help decoration-dotted underline' }), span('font-bold', () => this.compiler.compiled?.spellranks?.arts ?? 0) ]) : undefined,
() => (this.compiled?.spellranks?.precision ?? 0) > 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 cursor-help decoration-dotted underline' }), span('font-bold', () => this.compiled?.spellranks?.precision ?? 0) ]) : undefined,
() => (this.compiled?.spellranks?.knowledge ?? 0) > 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 cursor-help decoration-dotted underline' }), span('font-bold', () => this.compiled?.spellranks?.knowledge ?? 0) ]) : undefined,
() => (this.compiled?.spellranks?.instinct ?? 0) > 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 cursor-help decoration-dotted underline' }), span('font-bold', () => this.compiled?.spellranks?.instinct ?? 0) ]) : undefined,
])
])
]),
@@ -1804,7 +1801,7 @@ export class CharacterSheet
])
]);
reactivity(this.compiler.character, (c) => {
reactivity(this.character, (c) => {
if(c)
{
c.notes ??= { public: '', private: '' };
@@ -1819,7 +1816,7 @@ export class CharacterSheet
}
edit()
{
const editingData = reactive({
const editingData: Record<number, string[]> = reactive({
1: ["Peuple", "Entrainement", "Compétences", "Aspect", "Spécialisations"],
2: ["Entrainement"],
3: ["Entrainement"],
@@ -1843,41 +1840,65 @@ export class CharacterSheet
})
this.editingDOM = div('flex flex-1 max-w-full flex-col align-center relative', [
div('h-full w-0 border-l-2 border-light-35 dark:border-dark-35 absolute z-0 left-[89px]'),
...Array(20).fill(0).map((_, i) => div(() => ['flex flex-row gap-4', { 'opacity-30': (this.compiler.character?.level ?? 0) < (i + 1) }], [
...Array(20).fill(0).map((_, i) => div(() => ['flex flex-row gap-4', { 'opacity-30': (this.character?.level ?? 0) < (i + 1) }], [
div('flex flex-row gap-2 justify-end items-start w-24 py-4', [
div('flex flex-row items-center gap-2', [ span('italic text-xs tracking-tight text-end', `Niveau ${i + 1}`), span('w-3 h-3 rounded-full items-center justify-center bg-light-50 dark:bg-dark-50 z-10') ]),
]),
div('flex flex-col px-2 items-center justify-center divide-y divide-light-35 dark:divide-dark-35', { list: [], render: (e, _c) => _c ?? span('flex px-2 w-full py-2 items-center justify-center cursor-pointer hover:bg-light-20 dark:hover:bg-dark-20', e) }),
div('flex flex-col px-2 items-center justify-center divide-y divide-light-35 dark:divide-dark-35', { list: editingData[i + 1], render: (e, _c) => _c ?? span('flex px-2 w-full py-2 items-center justify-center cursor-pointer hover:bg-light-20 dark:hover:bg-dark-20', e) }),
]))
]);
}
saveVariables()
{
if(!this._character) return;
clearTimeout(this._variableDebounce);
this._variableDebounce = setTimeout(() => {
if(!this._character) return;
useRequestFetch()(`/api/character/${this._character.id}/variables`, {
method: 'POST',
body: raw(this._character.variables),
}).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
})
}, 2000);
}
saveNotes()
{
if(!this._character) return Promise.resolve();
return useRequestFetch()(`/api/character/${this._character.id}/notes`, {
method: 'POST',
body: this._character.notes,
}).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
});
}
async saveEdits()
{
if(!this.compiler.character)
if(!this.character)
return;
if(!this.state.exists)
{
const result = await useRequestFetch()(`/api/character`, {
method: 'post',
body: this.compiler.character,
body: this.character,
onResponseError: (e) => {
Toaster.add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', closeable: true, duration: 25000, timer: true });
this.compiler.character!.id = -1;
this.character!.id = -1;
}
});
if(result !== undefined) this.compiler.character.id = this.compiler.compiled.id = result as number;
if(result !== undefined) this.character.id = this.compiled.id = result as number;
Toaster.add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
useRouter().push({ name: 'character-id', params: { id: this.compiler.compiled.id } });
useRouter().push({ name: 'character-id', params: { id: this.compiled.id } });
this.state.edit = false;
}
else
{
await useRequestFetch()(`/api/character/${this.compiler!.character.id}`, {
await useRequestFetch()(`/api/character/${this.character.id}`, {
method: 'post',
body: this.compiler!.character,
body: this.character,
onResponseError: (e) => {
Toaster.add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', closeable: true, duration: 25000, timer: true });
}
@@ -1886,7 +1907,7 @@ export class CharacterSheet
this.state.edit = false;
}
}
healthPanel(character: CompiledCharacter | undefined)
healthPanel()
{
const inputs = reactive({
health: {
@@ -1902,54 +1923,55 @@ export class CharacterSheet
},
mana: 0,
});
const armor = this.compiler.armor;
const container = div("border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-[480px] flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
div('flex flex-row justify-between items-center', [
div('flex flex-row gap-8 items-center', [ span('text-xl font-bold', 'Edititon de vie'), () => !character ? span('text-xl font-bold', '-') : div('flex flex-row items-center gap-1', [ span('text-xl font-bold', () => (character.health - character.variables.health)), text('/'), text(() => character.health) ]) ]),
div('flex flex-row gap-8 items-center', [ span('text-xl font-bold', 'Edititon de vie'), () => !this.compiled ? span('text-xl font-bold', '-') : div('flex flex-row items-center gap-1', [ span('text-xl font-bold', () => (this.compiled.health - this.compiled.variables.health)), text('/'), text(() => this.compiled.health) ]) ]),
tooltip(button(icon("radix-icons:cross-1", { width: 24, height: 24 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left")
]),
foldable([
div('flex flex-col w-full gap-2 ms-2 ps-4 border-l border-light-35 dark:border-dark-35', DAMAGE_TYPES.map(e => div('flex flex-row justify-between items-center', [
span('text-lg', damageTypeTexts[e]), div('flex flew-row gap-4 justify-end', [ () => /* Res/Vul/Immun */ div('w-8'), numberpicker({ defaultValue: () => inputs.health[e], input: v => { inputs.health[e] = v; inputs.health.sum = DAMAGE_TYPES.reduce((p, v) => p + inputs.health[v], 0) }, min: 0, class: 'h-8 !m-0' }), div('w-8') ]),
div('flex flex-col w-full gap-2 ms-2 ps-4 border-l border-light-35 dark:border-dark-35', DAMAGE_TYPES.map((e) => div('flex flex-row justify-between items-center', [
span('text-lg', damageTypeTexts[e]), div('flex flew-row gap-4 items-center justify-end', [ () => this.compiled.bonus.damage[e] ? tooltip(span('w-8 font-bold', protectionTexts[this.compiled.bonus.damage[e] as 'resistance' | 'immunity' | 'vulnerability'].summary), protectionTexts[this.compiled.bonus.damage[e] as 'resistance' | 'immunity' | 'vulnerability'].detail, 'left') : div('w-8'), numberpicker({ defaultValue: () => inputs.health[e], input: v => { inputs.health[e] = v; inputs.health.sum = DAMAGE_TYPES.reduce((p, v) => p + inputs.health[v], 0) }, min: 0, class: 'h-8 !m-0' }), div('w-8') ]),
])))
], [
div('flex flex-row justify-between items-center', [
() => !character ? undefined : span('text-lg', 'Total'), div('flex flew-row gap-4 justify-end', [
() => armor ? tooltip(button(div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:shoulder-armor', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => `${armor.current}/${armor.max} (${[armor.flat > 0 ? '-' + armor.flat : undefined, armor.percent > 0 ? armor.percent + '%' : undefined].filter(e => !!e).join('/')})`) ]), () => {
//TODO
}, 'px-2 h-8 border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:border-light-red dark:focus:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red'), 'Dégats', 'left') : undefined,
div('flex flex-row justify-between items-center', [
() => !this.compiled ? undefined : span('text-lg', 'Total'), div('flex flew-row gap-4 justify-end', [
() => this.armor.state ? tooltip(button(div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:shoulder-armor', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => `${this.armor.current}/${this.armor.max} (${[this.armor.flat > 0 ? '-' + this.armor.flat : undefined, this.armor.percent > 0 ? this.armor.percent + '%' : undefined].filter(e => !!e).join('/')})`) ]), () => {
const value = clamp(this.armor.percent > 0 ? Math.floor(inputs.health.sum * clamp(this.armor.percent / 100, 0, 1)) : clamp(inputs.health.sum, 0, this.armor.flat), 0, this.armor.current);
(this.armor.state!.state as ArmorState).loss += value;
inputs.health.sum -= value;
}, 'px-2 h-8 border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:border-light-red dark:focus:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red', () => this.armor.current === 0), 'Dégats', 'left') : undefined,
tooltip(button(icon('radix-icons:minus', { width: 16, height: 16 }), () => {
character!.variables.health += inputs.health.sum;
this.compiled.variables.health += inputs.health.sum;
inputs.health.sum = 0;
DAMAGE_TYPES.forEach(e => inputs.health[e] = 0);
this.compiler.saveVariables();
this.saveVariables();
}, 'w-8 h-8 border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:border-light-red dark:focus:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red'), 'Dégats', 'left'),
numberpicker({ defaultValue: () => inputs.health.sum, input: v => { inputs.health.sum = v }, min: 0, disabled: () => inputs.health.open, class: 'h-8 !m-0' }),
tooltip(button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
character!.variables.health = Math.max(character!.variables.health - inputs.health.sum, 0);
this.compiled.variables.health = Math.max(this.compiled.variables.health - inputs.health.sum, 0);
inputs.health.sum = 0;
DAMAGE_TYPES.forEach(e => inputs.health[e] = 0);
this.compiler.saveVariables();
this.saveVariables();
}, 'w-8 h-8 border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green focus:border-light-green dark:focus:border-dark-green focus:shadow-light-green dark:focus:shadow-dark-green'), 'Soin', 'left'),
])
])
], { class: { container: 'gap-2', title: 'ps-2' }, open: false, onFold: v => { inputs.health.open = v; if(v) { inputs.health.sum = 0; }} }),
() => !character ? undefined : div('flex flex-row justify-between items-center', [
div('flex flex-row gap-8 items-center', [ span('text-xl font-bold', 'Mana'), div('flex flex-row items-center gap-1', [ span('text-xl font-bold', () => (character.mana - character.variables.mana)), text('/'), text(() => character.mana) ]) ]),
() => div('flex flex-row justify-between items-center', [
div('flex flex-row gap-8 items-center', [ span('text-xl font-bold', 'Mana'), div('flex flex-row items-center gap-1', [ span('text-xl font-bold', () => (this.compiled.mana - this.compiled.variables.mana)), text('/'), text(() => this.compiled.mana) ]) ]),
div('flex flex-row gap-4 justify-end', [
tooltip(button(icon('radix-icons:minus', { width: 16, height: 16 }), () => {
character.variables.mana += inputs.mana;
this.compiled.variables.mana += inputs.mana;
inputs.mana = 0;
this.compiler.saveVariables();
this.saveVariables();
}, 'w-8 h-8 border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red focus:border-light-red dark:focus:border-dark-red focus:shadow-light-red dark:focus:shadow-dark-red'), 'Dégats', 'left'),
numberpicker({ defaultValue: () => inputs.mana, input: v => { inputs.mana = v }, min: 0, class: 'h-8 !m-0' }),
tooltip(button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
character.variables.mana = Math.max(character.variables.mana - inputs.mana, 0);
this.compiled.variables.mana = Math.max(this.compiled.variables.mana - inputs.mana, 0);
inputs.mana = 0;
this.compiler.saveVariables();
this.saveVariables();
}, 'w-8 h-8 border-light-green dark:border-dark-green hover:border-light-green dark:hover:border-dark-green focus:border-light-green dark:focus:border-dark-green focus:shadow-light-green dark:focus:shadow-dark-green'), 'Soin', 'left'),
])
]),
@@ -1966,8 +1988,6 @@ export class CharacterSheet
}
actionsTab()
{
const character = this.compiler.compiled;
return [
div('flex flex-col gap-8', [
div('flex flex-col gap-2', [
@@ -1982,7 +2002,7 @@ export class CharacterSheet
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 font-semibold', 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 } }),
]), list: () => character ? character.lists.action ?? [] : [] }),
]), list: () => this.compiled ? this.compiled.lists.action ?? [] : [] }),
]),
]),
div('flex flex-col gap-2', [
@@ -1997,7 +2017,7 @@ export class CharacterSheet
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 font-semibold', 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 } }),
]), list: () => character ? character.lists.reaction ?? [] : [] }),
]), list: () => this.compiled ? this.compiled.lists.reaction ?? [] : [] }),
]),
]),
div('flex flex-col gap-2', [
@@ -2011,19 +2031,19 @@ export class CharacterSheet
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 font-semibold', text: config.freeaction[e]?.name }) ]),
markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
]), list: () => character ? character.lists.freeaction ?? [] : [] })
]), list: () => this.compiled ? this.compiled.lists.freeaction ?? [] : [] })
]),
]),
]),
]
}
abilitiesTab(character: CompiledCharacter | undefined)
abilitiesTab()
{
return [ div('flex flex-col gap-4', [
foldable(() => [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 font-semibold', text: config.passive[e]?.name }) ]),
markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.passive })], [
]), list: this.compiled.lists.passive })], [
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-bold', text: "Aptitudes" }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
@@ -2032,7 +2052,7 @@ export class CharacterSheet
foldable(() => [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 font-semibold', text: config.dedication[e]?.name }) ]),
markdown(getText(config.features[e]?.description), undefined, { tags: { a: preview } }),
]), list: character.lists.dedication })], [
]), list: this.compiled.lists.dedication })], [
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-bold', text: "Spécialisations" }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
@@ -2040,14 +2060,14 @@ export class CharacterSheet
], { open: false }),
]) ];
}
spellTab(character: CompiledCharacter | undefined)
spellTab()
{
const preference = reactive({
sort: localStorage.getItem('character-sort') ?? 'rank-asc',
sort: localStorage.getItem('this.compiled-sort') ?? 'rank-asc',
} as { sort: `${'rank'|'type'|'element'|'cost'|'range'|'speed'}-${'asc'|'desc'}` | '' });
const sort = (spells: string[]) => {
localStorage.setItem('character-sort', preference.sort);
localStorage.setItem('this.compiled-sort', preference.sort);
const _spells = Object.keys(config.spells);
spells.sort((a, b) => _spells.indexOf(a) - _spells.indexOf(b));
@@ -2071,7 +2091,7 @@ export class CharacterSheet
}
};
const panel = this.spellPanel(character);
const panel = this.spellPanel();
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 ]),
@@ -2086,8 +2106,8 @@ export class CharacterSheet
div('flex flex-col gap-2 h-full', [
div('flex flex-row justify-end 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.spellslots }], text: () => `${character.variables.spells.length}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', character.variables.spells.length > 1 ? 's' : '') }),
button(text('Modifier'), () => panel.show(), 'py-1 px-4'),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.compiled.variables.spells.length !== this.compiled.spellslots }], text: () => `${this.compiled.variables.spells.length}/${this.compiled.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', this.compiled.variables.spells.length > 1 ? 's' : '') }),
button(text('Modifier'), () => panel.show(), 'py-1 px-4', () => this.state.edit),
tooltip(button(icon('ph:arrows-down-up', { width: 16, height: 16 }), sorter, 'p-1'), 'Trier par', 'right')
])
]),
@@ -2095,9 +2115,7 @@ export class CharacterSheet
if(_c) return _c;
const spell = config.spells[e] as SpellConfig | undefined;
if(!spell)
return;
if(!spell) return;
return div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${spell.cost ?? 0} mana` }) ]),
@@ -2107,13 +2125,13 @@ export class CharacterSheet
]),
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(spell.description) ]),
])
}, list: () => sort([...(character.lists.spells ?? []), ...character.variables.spells]) }),
}, list: () => sort([...(this.compiled.lists.spells ?? []), ...this.compiled.variables.spells]) }),
])
]
}
spellPanel(character: CompiledCharacter | undefined)
spellPanel()
{
const spells = character.variables.spells;
const spells = this.compiled.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: [],
@@ -2127,7 +2145,7 @@ export class CharacterSheet
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", [
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.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', spells.length > 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}/${this.compiled.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', spells.length > 1 ? 's' : '')) ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left") ])
@@ -2143,7 +2161,7 @@ export class CharacterSheet
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(this.compiled.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;
@@ -2177,12 +2195,12 @@ export class CharacterSheet
text("/"),
dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` })
]),
button(text(() => spells.includes(spell.id) ? 'Supprimer' : character.lists.spells?.includes(spell.id) ? 'Inné' : 'Ajouter'), () => {
button(text(() => spells.includes(spell.id) ? 'Supprimer' : this.compiled.lists.spells?.includes(spell.id) ? 'Inné' : 'Ajouter'), () => {
const idx = spells.findIndex(e => e === spell.id);
if(idx !== -1) spells.splice(idx, 1);
else spells.push(spell.id);
this.compiler.saveVariables();
this.saveVariables();
}, "px-2 py-1 text-sm font-normal"),
]),
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } })
@@ -2198,15 +2216,15 @@ export class CharacterSheet
container.setAttribute('data-state', 'inactive');
}};
}
itemsTab(character: CompiledCharacter | undefined)
itemsTab()
{
const items = character.variables.items;
const panel = this.itemsPanel(character);
const enchant = this.enchantPanel(character);
const items = this.compiled.variables.items;
const panel = this.itemsPanel();
const enchant = this.enchantPanel();
const money = {
readonly: dom('div', { listeners: { click: () => { money.readonly.replaceWith(money.edit); money.edit.focus(); } }, class: 'cursor-pointer border border-transparent hover:border-light-40 dark:hover:border-dark-40 px-2 py-px flex flex-row gap-1 items-center' }, [ span('text-lg font-bold', () => character.variables.money.toLocaleString(undefined, { useGrouping: true })), icon('ph:coin', { width: 16, height: 16 }) ]),
edit: numberpicker({ defaultValue: character.variables.money, change: v => { character.variables.money = v; this.compiler.saveVariables(); money.edit.replaceWith(money.readonly); }, blur: v => { character.variables.money = v; this.compiler.saveVariables(); money.edit.replaceWith(money.readonly); }, min: 0, class: 'w-24' }),
readonly: dom('div', { listeners: { click: () => { money.readonly.replaceWith(money.edit); money.edit.focus(); } }, class: 'cursor-pointer border border-transparent hover:border-light-40 dark:hover:border-dark-40 px-2 py-px flex flex-row gap-1 items-center' }, [ span('text-lg font-bold', () => this.compiled.variables.money.toLocaleString(undefined, { useGrouping: true })), icon('ph:coin', { width: 16, height: 16 }) ]),
edit: numberpicker({ defaultValue: this.compiled.variables.money, change: v => { this.compiled.variables.money = v; this.saveVariables(); money.edit.replaceWith(money.readonly); }, blur: v => { this.compiled.variables.money = v; this.saveVariables(); money.edit.replaceWith(money.readonly); }, min: 0, class: 'w-24' }),
};
return [
@@ -2216,12 +2234,12 @@ export class CharacterSheet
div('flex flex-row gap-1 items-center', [ span('italic text-sm', 'Argent'), money.readonly ]),
]),
div('flex flex-row justify-end items-center gap-8', [
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.compiler!.power > character.itempower }], text: () => `Puissance magique: ${this.compiler!.power}/${character.itempower}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.compiler!.weight > (character.capacity === false ? 0 : character.capacity) }], text: () => `Poids total: ${this.compiler!.weight}/${character.capacity}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.power > this.compiled.itempower }], text: () => `Puissance magique: ${this.power}/${this.compiled.itempower}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.weight > (this.compiled.capacity === false ? 0 : this.compiled.capacity) }], text: () => `Poids total: ${this.weight}/${this.compiled.capacity}` }),
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, _c) => {
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: this.compiled.variables.items, render: (e, _c) => {
if(_c) return _c;
const item = config.items[e.id];
@@ -2236,7 +2254,7 @@ export class CharacterSheet
markdown(getText(item.description)),
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 px-2 rounded-full py-px !bg-opacity-20', { 'border-accent-blue bg-accent-blue': !e.cursed, 'border-light-purple bg-light-purple dark:border-dark-purple dark:bg-dark-purple': e.cursed }], [ span('text-sm font-semibold tracking-tight', 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', [
this.compiler.character?.campaign ? button(text('Partager'), () => {
this.character.campaign ? button(text('Partager'), () => {
}, 'px-2 text-sm h-5 box-content') : undefined,
button(icon(() => e.amount === 1 ? 'radix-icons:trash' : 'radix-icons:minus', { width: 12, height: 12 }), () => {
@@ -2246,7 +2264,7 @@ export class CharacterSheet
items[idx]!.amount--;
if(items[idx]!.amount <= 0) items.splice(idx, 1);
this.compiler.saveVariables();
this.saveVariables();
}, 'p-1'),
button(icon('radix-icons:plus', { width: 12, height: 12 }), () => {
const idx = items.findIndex(_e => _e === e);
@@ -2256,7 +2274,7 @@ export class CharacterSheet
else if(items.find(_e => _e === e)) items.find(_e => _e === e)!.amount++;
else items.push(stateFactory(item));
this.compiler.saveVariables();
this.saveVariables();
}, 'p-1'),
() => !item.capacity ? undefined : button(text("Enchanter"), () => {
enchant.show(e);
@@ -2265,12 +2283,15 @@ export class CharacterSheet
], [ div('flex flex-row justify-between', [
div('flex flex-row items-center gap-y-1 gap-x-4 flex-wrap', [
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
if(v && config.items[e.id]?.category === 'armor' && this.compiled.variables.items.find(e => config.items[e.id]?.category === 'armor' && e.equipped))
return Toaster.add({ content: "Vous ne pouvez equipper qu'une seule armure à la fois.", duration: 5000, timer: true, type: 'info' }), false;
e.equipped = v;
this.compiler.enchant(e);
this.enchant(e);
}, 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))) ]),
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', () => stringifyRoll(parseDice(`${item.damage.value}${(e.state as WeaponState)?.attack ? '+' + (e.state as WeaponState).attack : ''}`), character.modifier, true)), 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 }) ]) :
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', () => stringifyRoll(parseDice(`${item.damage.value}${(e.state as WeaponState)?.attack ? '+' + (e.state as WeaponState).attack : ''}`), this.compiled.modifier, true)), 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', [
@@ -2285,7 +2306,7 @@ export class CharacterSheet
])
];
}
itemsPanel(character: CompiledCharacter | undefined)
itemsPanel()
{
const filters: { category: Category[], rarity: Rarity[], name: string, power: { min: number, max: number } } = reactive({
category: [],
@@ -2298,7 +2319,7 @@ export class CharacterSheet
div("flex flex-row justify-between items-center mb-4", [
dom("h2", { class: "text-xl font-bold", text: "Gestion de l'inventaire" }),
div('flex flex-row gap-8 items-center justify-end', [
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.compiler!.weight > (character.capacity === false ? 0 : character.capacity) }], text: () => `Poids total: ${this.compiler!.weight}/${character.capacity}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.weight > (this.compiled.capacity === false ? 0 : this.compiled.capacity) }], text: () => `Poids total: ${this.weight}/${this.compiled.capacity}` }),
tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
@@ -2331,12 +2352,12 @@ export class CharacterSheet
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('game-icons:battery-pack', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.charge ? `${item.charge}` : '-') ]),
div('flex flex-row w-16 gap-2 justify-between items-center px-2', [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', item.price ? `${item.price}` : '-') ]),
button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
const list = this.compiler.character!.variables.items;
const list = this.compiled.variables.items;
if(item.equippable) list.push(stateFactory(item));
else if(list.find(e => e.id === item.id)) list.find(e => e.id === item.id)!.amount++;
else list.push(stateFactory(item));
this.compiler.saveVariables();
this.saveVariables();
}, 'p-1 !border-solid !border-r'),
]),
])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } })
@@ -2352,7 +2373,7 @@ export class CharacterSheet
container.setAttribute('data-state', 'inactive');
}};
}
enchantPanel(character: CompiledCharacter | undefined)
enchantPanel()
{
const current = reactive({
item: undefined as ItemState | undefined,
@@ -2380,7 +2401,7 @@ export class CharacterSheet
dom("h2", { class: "text-xl font-bold", text: "Enchantements" }),
div('flex flex-row gap-8 items-center justify-end', [
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': current.item && config.items[current.item.id] !== undefined ? itempower() > (config.items[current.item.id]!.capacity ?? 0) : false }], text: () => `Puissance de l'objet: ${current.item && config.items[current.item.id] !== undefined ? itempower() : false}/${current.item ? (config.items[current.item.id]!.capacity ?? 0) : 0}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.compiler!.power > character!.itempower }], text: () => `Puissance du personnage: ${this.compiler!.power}/${character.itempower}` }),
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': this.power > this.compiled!.itempower }], text: () => `Puissance du personnage: ${this.power}/${this.compiled.itempower}` }),
tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
@@ -2400,7 +2421,7 @@ export class CharacterSheet
else
current.item!.enchantments?.splice(idx, 1);
this.compiler.enchant(current.item!);
this.enchant(current.item!);
}, 'p-1 !border-solid !border-r'),
]),
])], { open: false, class: { icon: 'px-2', container: 'border border-light-35 dark:border-dark-35 p-1 gap-2', content: 'px-2 pb-1' } })
@@ -2417,23 +2438,23 @@ export class CharacterSheet
container.setAttribute('data-state', 'inactive');
}};
}
aspectTab(character: CompiledCharacter | undefined)
aspectTab()
{
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-between items-center', [
div('flex flex-row gap-12 items-center', [
span('text-lg font-semibold', config.aspects[character.aspect.id]?.name), div('flex flex-row items-center gap-2', [ text('Transformé'), checkbox({ defaultValue: character.variables.transformed, change: v => character.variables.transformed = v, }) ]),
span('text-lg font-semibold', config.aspects[this.compiled.aspect.id]?.name), div('flex flex-row items-center gap-2', [ text('Transformé'), checkbox({ defaultValue: this.compiled.variables.transformed, change: v => this.compiled.variables.transformed = v, }) ]),
]),
div('flex flex-row gap-8 items-center', [
text('Difficulté: '), span('text-lg font-semibold', config.aspects[character.aspect.id]?.difficulty),
text('Difficulté: '), span('text-lg font-semibold', config.aspects[this.compiled.aspect.id]?.difficulty),
]),
]),
div(() => ({ 'opacity-20': !character.variables.transformed }), [ markdown(getText(config.aspects[character.aspect.id]?.description), undefined, { tags: { a: preview } }), ]),
div(() => ({ 'opacity-20': !this.compiled.variables.transformed }), [ markdown(getText(config.aspects[this.compiled.aspect.id]?.description), undefined, { tags: { a: preview } }), ]),
])
]
}
effectsTab(character: CompiledCharacter | undefined)
effectsTab()
{
return [

View File

@@ -36,21 +36,24 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise
return state;
}
export function button(content: Node | NodeChildren, onClick?: (this: HTMLElement) => void, cls?: Class)
export function button(content: Node | NodeChildren, onClick?: (this: HTMLElement) => void, cls?: Class, disabled?: Reactive<boolean>)
{
const btn = dom('button', { class: [`inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick.bind(btn)()) } }, Array.isArray(content) ? content : [content]);
let disabled = false;
let _disabled = false;
Object.defineProperty(btn, 'disabled', {
get: () => disabled,
get: () => _disabled,
set: (v) => {
disabled = !!v;
btn.toggleAttribute('disabled', disabled);
_disabled = !!v;
btn.toggleAttribute('disabled', _disabled);
}
})
});
disabled && reactivity(disabled, (d) => {
btn.disabled = d;
});
return btn;
}
export function buttongroup<T extends any>(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean | void })
@@ -603,7 +606,7 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
return element;
}
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void, disabled?: Reactive<boolean>, class?: { container?: Class, icon?: Class } })
export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HTMLElement, value: boolean) => void | boolean, disabled?: Reactive<boolean>, class?: { container?: Class, icon?: Class } })
{
let state = settings?.defaultValue ?? false;
const element = dom("div", { class: [`group w-6 h-6 box-content flex items-center justify-center border border-light-50 dark:border-dark-50 bg-light-20 dark:bg-dark-20
@@ -613,9 +616,11 @@ export function checkbox(settings?: { defaultValue?: boolean, change?: (this: HT
if(this.hasAttribute('data-disabled'))
return;
state = !state;
element.setAttribute('data-state', state ? "checked" : "unchecked");
settings?.change && settings.change.bind(this)(state);
if(settings?.change && settings.change.bind(this)(!state) !== false)
{
state = !state;
element.setAttribute('data-state', state ? "checked" : "unchecked");
}
}
}
}, [ icon('radix-icons:check', { width: 14, height: 14, class: ['hidden group-data-[state="checked"]:block data-[disabled]:text-light-50 dark:data-[disabled]:text-dark-50', settings?.class?.icon] }), ]);

View File

@@ -141,13 +141,13 @@ export class Content
const overview = await Content.read('overview', { create: true });
try
{
Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
Content._overview = reactive(parse<Record<string, Omit<LocalContent, 'content'>>>(overview));
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => { p[v.path] = v.id; return p; }, {} as Record<string, string>);
await Content.pull();
}
catch(e)
{
Content._overview = {};
Content._overview = reactive({});
await Content.pull(true);
}
@@ -284,11 +284,12 @@ export class Content
}
static async push()
{
const blocked = (await useRequestFetch()('/api/file/overview', { method: 'POST', body: Object.values(Content._overview), cache: 'no-cache' }));
const requested = (await useRequestFetch()('/api/file/overview', { method: 'POST', body: Object.values(Content._overview), cache: 'no-cache' }));
for(const [id, value] of Object.entries(Content._overview).filter(e => !blocked.includes(e[0])))
for(let id of requested)
{
if(value.type === 'folder')
const value = Content.get(id);
if(!value || value.type === 'folder')
continue;
Content.queue.queue(() => Content.read(id).then(e => {
@@ -651,6 +652,7 @@ export class Editor
this.tree.update();
},
redo: (action) => {
this.tree.tree.remove(action.element.id);
if(this.selected === action.element) this.select();
action.element.cleanup();
@@ -741,11 +743,11 @@ export class Editor
{
const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length;
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
this.history.add('overview', [{ element: item, from: undefined, to: nextTo.order + 1, event: 'add' }]);
}
private remove(item: LocalContent & { element?: HTMLElement })
{
this.history.add('overview', 'remove', [{ element: item, from: item.order, to: undefined }], true);
this.history.add('overview', [{ element: item, from: item.order, to: undefined, event: 'remove' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
}
private rename(item: LocalContent & { element?: HTMLElement })
{
@@ -760,7 +762,7 @@ export class Editor
input.parentElement?.replaceChild(text!, input);
input.remove();
if(value !== item.title) this.history.add('overview', 'rename', [{ element: item, from: item.title, to: value }], true);
if(value !== item.title) this.history.add('overview', [{ element: item, from: item.title, to: value, event: 'rename' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
}
}
const text = item.element!.children[0]?.children[1];
@@ -773,13 +775,13 @@ export class Editor
{
cancelPropagation(e);
this.history.add('overview', 'navigable', [{ element: item, from: item.navigable, to: !item.navigable }], true);
this.history.add('overview', [{ element: item, from: item.navigable, to: !item.navigable, event: 'navigable' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
}
private togglePrivate(e: Event, item: LocalContent & { element?: HTMLElement })
{
cancelPropagation(e);
this.history.add('overview', 'private', [{ element: item, from: item.private, to: !item.private }], true);
this.history.add('overview', [{ element: item, from: item.private, to: !item.private, event: 'private' }, { element: item, from: item.timestamp, to: new Date(), event: 'timestamp' }], true);
}
private setupDnD(): CleanupFn
{
@@ -884,13 +886,13 @@ export class Editor
return;
if (instruction.type === 'reorder-above')
this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order }}], true);
this.history.add('overview', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order }, event: 'move' }, { element: sourceItem, from: sourceItem.timestamp, to: new Date(), event: 'timestamp' }], true);
if (instruction.type === 'reorder-below')
this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order + 1 }}], true);
this.history.add('overview', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order + 1 }, event: 'move' }, { element: sourceItem, from: sourceItem.timestamp, to: new Date(), event: 'timestamp' }], true);
if (instruction.type === 'make-child' && targetItem.type === 'folder')
this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: targetItem, order: 0 }}], true);
this.history.add('overview', [{ element: sourceItem, from: from, to: { parent: targetItem, order: 0 }, event: 'move' }, { element: sourceItem, from: sourceItem.timestamp, to: new Date(), event: 'timestamp' }], true);
}
private render<T extends FileType>(item: LocalContent<T>): Node
{
@@ -923,6 +925,14 @@ export class Editor
{
this.cleanup && this.cleanup();
}
undo()
{
this.history.undo();
}
redo()
{
this.history.redo();
}
}
export function getPath(item: Recursive<Omit<LocalContent, 'content'>>): string

View File

@@ -1,10 +1,10 @@
import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character";
import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureState, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character";
import { div, dom, icon, span, text, type NodeChildren } from "#shared/dom";
import { MarkdownEditor } from "#shared/editor";
import { preview } from "#shared/proses";
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, colorByRarity, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, masteryTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, subnameFactory, weaponTypeTexts } from "#shared/character";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, colorByRarity, DAMAGE_TYPES, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, masteryTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, subnameFactory, weaponTypeTexts } from "#shared/character";
import characterConfig from "#shared/character-config.json";
import { getID } from "#shared/general";
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown";
@@ -600,7 +600,7 @@ export class HomebrewBuilder
}
}
type FeatureOption = Partial<FeatureValue | FeatureEquipment | FeatureList | FeatureChoice | FeatureTree> & { id: string };
type FeatureOption = Partial<FeatureValue | FeatureState | FeatureEquipment | FeatureList | FeatureChoice | FeatureTree> & { id: string };
class FeatureEditor
{
private _list: Record<string, FeatureOption> | FeatureOption[];
@@ -701,6 +701,8 @@ class FeatureEditor
{
case 'value':
return this.editValue(buffer as Partial<FeatureValue>);
case 'state':
return this.editState(buffer as Partial<FeatureState>);
case 'list':
return this.editList(buffer as Partial<FeatureList>);
case 'choice':
@@ -731,6 +733,16 @@ class FeatureEditor
div('px-2 py-1 flex items-center flex-1', [summaryText])
] };
}
private editState(buffer: Partial<FeatureState>)
{
const summaryText = text(textFromEffect(buffer));
return { top: [
input('text', { defaultValue: buffer.value, change: e => { buffer.value = e; summaryText.textContent = textFromEffect(buffer); } }),
], bottom: [
div('px-2 py-1 flex items-center flex-1', [summaryText])
] };
}
private editList(buffer: Partial<FeatureList>)
{
let list: Option<string>[] = [];
@@ -1056,8 +1068,15 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
{ text: 'Mod. de curiosité', effects: [ { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } ] },
{ text: 'Mod. de charisme', effects: [ { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } ] },
{ text: 'Mod. de psyché', effects: [ { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } ] }
]} as Partial<FeatureItem>}
]} as Partial<FeatureChoice>}
] },
{ text: 'Type de dégat', value: DAMAGE_TYPES.map(e => ({
text: damageTypeTexts[e], value: [
{ text: `${damageTypeTexts[e]} > Résistance`, value: { category: 'state', property: `bonus/damage/${e}`, value: 'resistance' } },
{ text: `${damageTypeTexts[e]} > Immunité`, value: { category: 'state', property: `bonus/damage/${e}`, value: 'immunity' } },
{ text: `${damageTypeTexts[e]} > Vulnérabilité`, value: { category: 'state', property: `bonus/damage/${e}`, value: 'vulnerability' } }
]
})) },
{ text: 'Jet de résistance', value: [
{ text: 'Résistance > Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } },
{ text: 'Résistance > Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } },
@@ -1082,18 +1101,15 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
{ text: 'Rang > Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } },
{ text: 'Rang > Sorts de savoir', value: { category: 'value', property: 'spellranks/knowledge', operation: 'add', value: 1 } },
{ text: 'Rang > Sorts d\'instinct', value: { category: 'value', property: 'spellranks/instinct', operation: 'add', value: 1 } },
{ text: 'Rang > Œuvres', value: { category: 'value', property: 'spellranks/arts', operation: 'add', value: 1 } },
] },
{ text: 'Bonus par type', value: [
{ text: 'Bonus > Précision', value: { category: 'value', property: 'bonus/spells/type/precision', operation: 'add', value: 1 } },
{ text: 'Bonus > Savoir', value: { category: 'value', property: 'bonus/spells/type/knowledge', operation: 'add', value: 1 } },
{ text: 'Bonus > Instinct', value: { category: 'value', property: 'bonus/spells/type/instinct', operation: 'add', value: 1 } },
{ text: 'Bonus > Œuvres', value: { category: 'value', property: 'bonus/spells/type/arts', operation: 'add', value: 1 } },
{ text: 'Bonus > Choix par type', value: { category: 'choice', options: [
{ text: 'Précision', effects: [{ category: 'value', property: 'bonus/spells/type/precision', operation: 'add', value: 1 }] },
{ text: 'Savoir', effects: [{ category: 'value', property: 'bonus/spells/type/knowledge', operation: 'add', value: 1 }] },
{ text: 'Instinct', effects: [{ category: 'value', property: 'bonus/spells/type/instinct', operation: 'add', value: 1 }] },
{ text: 'Œuvres', effects: [{ category: 'value', property: 'bonus/spells/type/arts', operation: 'add', value: 1 }] },
] } as Partial<FeatureChoice> }
]},
{ text: 'Bonus par rang', value: [
@@ -1228,6 +1244,10 @@ function textFromEffect(effect: Partial<FeatureOption>): string
return `Inconnu ("${effect.property}")`;
}
else if(effect.category === 'state')
{
return `Inconnu (state)`;
}
else if(effect.category === 'list')
{
switch(effect.list)

View File

@@ -6,11 +6,11 @@ export type HistoryHandler = {
interface HistoryEvent
{
source: string;
event: string;
actions: HistoryAction[];
}
interface HistoryAction
{
event: string;
element: any;
from?: any;
to?: any;
@@ -47,8 +47,8 @@ export class History
return;
last.actions.forEach(e => {
this.handlers[last.source] && this.handlers[last.source].handlers[last.event]?.undo(e)
this.handlers[last.source] && this.handlers[last.source].any && this.handlers[last.source].any!(e);
this.handlers[last.source] && this.handlers[last.source]?.handlers[e.event]?.undo(e)
this.handlers[last.source] && this.handlers[last.source]?.any && this.handlers[last.source]?.any!(e);
});
this.position--;
@@ -68,18 +68,18 @@ export class History
}
last.actions.forEach(e => {
this.handlers[last.source] && this.handlers[last.source].handlers[last.event]?.redo(e)
this.handlers[last.source] && this.handlers[last.source].any && this.handlers[last.source].any!(e);
this.handlers[last.source] && this.handlers[last.source]?.handlers[e.event]?.redo(e)
this.handlers[last.source] && this.handlers[last.source]?.any && this.handlers[last.source]?.any!(e);
});
}
add(source: string, event: string, actions: HistoryAction[], apply: boolean = false)
add(source: string, actions: HistoryAction[], apply: boolean = false)
{
this.position++;
this.history.splice(this.position, history.length - this.position, { source, event, actions });
this.history.splice(this.position, history.length - this.position, { source, actions });
if(apply)
actions.forEach(e => {
this.handlers[source] && this.handlers[source].handlers[event]?.redo(e);
this.handlers[source] && this.handlers[source].handlers[e.event]?.redo(e);
this.handlers[source] && this.handlers[source].any && this.handlers[source].any(e);
});
}