Progress on tree features

This commit is contained in:
Clément Pons 2026-01-13 17:47:18 +01:00
parent f761e44569
commit 796b335b2e
5 changed files with 12182 additions and 70 deletions

View File

@ -58,14 +58,16 @@ export type CharacterVariables = {
money: number; money: number;
}; };
export type TreeStructure = { export type TreeStructure = {
name: string; name: string;
nodes: FeatureID[];
// { 'from_id': { 'pathname': 'to_id' } };
paths: Record<number, Record<string, number>>;
}; };
type CommonState = { type CommonState = {
capacity?: number; capacity?: number;
powercost?: number; powercost?: number;
}; };
type StateBufferKeys = typeof ITEM_BUFFER_KEYS[number];
type ArmorState = { loss: number, health?: number, absorb?: { flat?: number, percent?: 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 = { };
@ -77,7 +79,6 @@ 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>;
@ -198,7 +199,7 @@ export type FeatureEquipment = {
id: FeatureID; id: FeatureID;
category: "value"; category: "value";
operation: "add" | "set" | "min"; operation: "add" | "set" | "min";
property: StateBufferKeys; property: `item/${RecursiveKeyOf<(ArmorState | WeaponState | WondrousState | MundaneState) & CommonState>}`;
value: number | `modifier/${MainStat}` | false; value: number | `modifier/${MainStat}` | false;
} }
export type FeatureList = { export type FeatureList = {

BIN
db.sqlite

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@ import { getText } from "#shared/i18n";
import type { User } from "~/types/auth"; import type { User } from "~/types/auth";
import { MarkdownEditor } from "#shared/editor.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, reactivity } from '#shared/reactive';
const config = characterConfig as CharacterConfig; const config = characterConfig as CharacterConfig;
@ -26,7 +26,6 @@ export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_
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,
@ -184,7 +183,7 @@ export const elementTexts: Record<SpellElement, { class: string, text: string }>
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' }, psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
}; };
export const elementDom = (element: SpellElement) => dom("span", { export const elementDom = (element: SpellElement) => dom("span", {
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class], class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-1 py-px text-sm font-semibold`, elementTexts[element].class],
text: elementTexts[element].text text: elementTexts[element].text
}); });
export const alignmentTexts: Record<Alignment, string> = { export const alignmentTexts: Record<Alignment, string> = {
@ -336,8 +335,6 @@ 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
@ -378,29 +375,14 @@ 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);
} }
update(item: ItemState)
{
item.buffer ??= {};
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: variables, body: raw(this._character.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 });
}) })
@ -429,7 +411,7 @@ export class CharacterCompiler
config.features[feature]?.effect.forEach((effect) => this.undo(effect)); config.features[feature]?.effect.forEach((effect) => this.undo(effect));
} }
protected apply(feature?: FeatureItem | FeatureEquipment, item?: ItemState) protected apply(feature?: FeatureItem | FeatureEquipment)
{ {
if(!feature) if(!feature)
return; return;
@ -445,16 +427,15 @@ export class CharacterCompiler
return; return;
case "value": case "value":
const target = (item && ITEM_BUFFER_KEYS.includes(feature.property as any) ? item.buffer : this._buffer) as Record<string, PropertySum>; this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
target[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
target[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value }); this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
target[feature.property]!.min = -Infinity; this._buffer[feature.property]!.min = -Infinity;
target[feature.property]!._dirty = true; this._buffer[feature.property]!._dirty = true;
if(feature.property.startsWith('modifier/')) if(feature.property.startsWith('modifier/'))
Object.values(target).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty); Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
return; return;
case "choice": case "choice":
@ -468,7 +449,7 @@ export class CharacterCompiler
return; return;
} }
} }
protected undo(feature?: FeatureItem | FeatureEquipment, item?: ItemState) protected undo(feature?: FeatureItem | FeatureEquipment)
{ {
if(!feature) if(!feature)
return; return;
@ -484,17 +465,16 @@ export class CharacterCompiler
return; return;
case "value": case "value":
const target = (item && ITEM_BUFFER_KEYS.includes(feature.property as any) ? item.buffer : this._buffer) as Record<string, PropertySum>; this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
target[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
const idx = target[feature.property]!.list.findIndex(e => e.id === feature.id); const idx = this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id);
idx !== -1 && target[feature.property]!.list.splice(idx, 1); idx !== -1 && this._buffer[feature.property]!.list.splice(idx, 1);
target[feature.property]!.min = -Infinity; this._buffer[feature.property]!.min = -Infinity;
target[feature.property]!._dirty = true; this._buffer[feature.property]!._dirty = true;
if(feature.property.startsWith('modifier/')) if(feature.property.startsWith('modifier/'))
Object.values(target).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty); Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
return; return;
case "choice": case "choice":
@ -508,14 +488,14 @@ export class CharacterCompiler
return; return;
} }
} }
protected compile(queue: string[], _buffer: Record<string, PropertySum> = this._buffer, _target: any = this._result) protected compile(queue: string[])
{ {
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 = _buffer[property]; const buffer = this._buffer[property];
if(buffer && buffer._dirty === true) if(buffer && buffer._dirty === true)
{ {
@ -528,7 +508,7 @@ 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 = _buffer[item.value as string]!; const modifier = this._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
@ -563,13 +543,13 @@ export class CharacterCompiler
continue; continue;
const path = property.split("/"); const path = property.split("/");
const object = path.length === 1 ? _target : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, _target as any); const object = path.length === 1 ? this._result : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, this._result as any);
if(object.hasOwnProperty(path.slice(-1)[0]!)) if(object.hasOwnProperty(path.slice(-1)[0]!))
object[path.slice(-1)[0]!] = Math.max(sum, _buffer[property]!.min); object[path.slice(-1)[0]!] = Math.max(sum, this._buffer[property]!.min);
_buffer[property]!.value = Math.max(sum, _buffer[property]!.min); this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min);
_buffer[property]!._dirty = false; this._buffer[property]!._dirty = false;
} }
} }
} }
@ -1615,31 +1595,31 @@ export class CharacterSheet
]), ]),
() => 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', 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 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 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 cursor-help decoration-dotted underline' }) : 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 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : 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 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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', 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', class: 'text-sm text-light-70 dark:text-dark-70 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", [
() => 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, () => 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 cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.precision)) ]) : undefined,
() => 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, () => 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 cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.knowledge)) ]) : undefined,
() => 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, () => 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 cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.instinct)) ]) : undefined,
() => 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, () => 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 cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.arts)) ]) : undefined,
]) ])
]) ])
]), ]),
@ -1904,7 +1884,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 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-accent-blue px-2 rounded-full py-px bg-accent-blue 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'), () => {
@ -2059,10 +2039,11 @@ export class CharacterSheet
]), ]),
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('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 gap-4', [
span('italic text-sm', `Puissance magique: ${enchant.power}`), span('italic text-sm', `Puissance magique: ${enchant.power}`),
button(icon('radix-icons:plus', { width: 16, height: 16 }), () => { button(icon('radix-icons:plus', { width: 16, height: 16 }), () => {
current.item!.enchantments?.push(enchant.id); current.item!.enchantments?.push(enchant.id);
// TODO: Object.assign(current.item!.state, current.item!.enchantments?.reduce((p, id) => { config.enchantments[id]?.effect.filter(e => e.category === "value" && e.property.startsWith('item/')); return p; }, {}));
this.character?.saveVariables(); this.character?.saveVariables();
}, 'p-1 !border-solid !border-r'), }, 'p-1 !border-solid !border-r'),

View File

@ -15,7 +15,7 @@ import { reactive } from "./reactive";
type Category = ItemConfig['category']; type Category = ItemConfig['category'];
type Rarity = ItemConfig['rarity']; type Rarity = ItemConfig['rarity'];
const config = characterConfig as CharacterConfig; const config = reactive(characterConfig as CharacterConfig);
export class HomebrewBuilder export class HomebrewBuilder
{ {
private _container: RedrawableHTML; private _container: RedrawableHTML;
@ -32,6 +32,7 @@ export class HomebrewBuilder
{ id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() }, { id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() },
{ id: 'actions', title: [ text("Actions") ], content: () => this.actions() }, { id: 'actions', title: [ text("Actions") ], content: () => this.actions() },
{ id: 'items', title: [ text("Objets") ], content: () => this.items() }, { id: 'items', title: [ text("Objets") ], content: () => this.items() },
{ id: 'trees', title: [ text("Arbres") ], content: () => this.trees() },
], { focused: 'training', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } }); ], { focused: 'training', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } });
this._tabs.children[0]?.appendChild(tooltip(button(icon('radix-icons:clipboard'), () => this.save(), 'p-1'), 'Copier', 'bottom')); this._tabs.children[0]?.appendChild(tooltip(button(icon('radix-icons:clipboard'), () => this.save(), 'p-1'), 'Copier', 'bottom'));
@ -459,6 +460,31 @@ export class HomebrewBuilder
const optionHolder = div('grid grid-cols-3 gap-2', options.map(e => e.dom)); const optionHolder = div('grid grid-cols-3 gap-2', options.map(e => e.dom));
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), optionmenu([{ title: 'Objet inerte', click: () => add('mundane') }, { title: 'Armure', click: () => add('armor') }, { title: 'Arme', click: () => add('weapon') }, { title: 'Objet magique', click: () => add('wondrous') }], { position: 'left-start' }), 'p-1') ]), optionHolder ] ) ]; return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), optionmenu([{ title: 'Objet inerte', click: () => add('mundane') }, { title: 'Armure', click: () => add('armor') }, { title: 'Arme', click: () => add('weapon') }, { title: 'Objet magique', click: () => add('wondrous') }], { position: 'left-start' }), 'p-1') ]), optionHolder ] ) ];
} }
trees()
{
const add = () => {
const description = getID();
setText(description, "");
const id = getID();
config.features[id] = {
id,
description,
effect: [],
};
config.trees[editing.tree!]!.nodes.push(id);
}
const editing = reactive({ tree: undefined as string | undefined });
return [div('', [
() => editing.tree !== undefined ? undefined : div('flex flex-row gap-1 justify-start overflow-x-auto max-w-full', { list: Object.values(config.trees), render: (e, _c) => _c ?? div('grid grid-cols-2 gap-2 items-baseline w-64 border border-light-35 dark:border-dark-35 p-2', [ span('text-lg font-semibold tracking-thigh', e.name), div('flex flex-row justify-end', [ tooltip(button(icon('radix-icons:pencil-1', { width: 16, height: 16 }), () => editing.tree = e.name, 'p-1'), 'Modifier', 'left') ]), span('italic', `${Object.keys(e.nodes).length} nodes`) ]) }),
() => editing.tree === undefined ? undefined : div('', [
foldable([ div('flex flex-col gap-2', { list: config.trees[editing.tree]!.nodes, render: (e, _c) => _c ?? dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: () => {
FeaturePanel.edit(config.features[e]!).then(feature => {
config.features[e] = feature;
}).catch(e => {});
}}}, [ markdown(getText(config.features[e]!.description), undefined, { tags: { a: preview } }) ]) }) ], [ span('text-lg font-bold px-2', 'Nodes'), button(text('Nouvelle node'), () => add(), 'py-1 px-2') ], { open: true, class: { title: 'flex flex-row justify-between' } })
]),
])];
}
private save() private save()
{ {
navigator.clipboard.writeText(JSON.stringify(config)); navigator.clipboard.writeText(JSON.stringify(config));