obsidian-visualiser/shared/character.util.ts

594 lines
29 KiB
TypeScript

import type { Ability, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
import { z } from "zod/v4";
import characterConfig from './character-config.json';
import { button, loading } from "./proses";
import { div, dom, icon, text } from "./dom.util";
import { popper } from "./floating.util";
import { clamp } from "./general.util";
const config = characterConfig as CharacterConfig;
export const MAIN_STATS = ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] as const;
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const;
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const;
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const;
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const;
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
export const defaultCharacter: Character = {
id: -1,
name: "",
people: undefined,
level: 1,
health: 0,
mana: 0,
training: MAIN_STATS.reduce((p, v) => { p[v] = [[0, 0]]; return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
leveling: [[1, 0]],
abilities: {},
spells: [],
modifiers: {},
choices: {},
owner: -1,
visibility: "private",
};
export const mainStatTexts: Record<MainStat, string> = {
"strength": "Force",
"dexterity": "Dextérité",
"constitution": "Constitution",
"intelligence": "Intelligence",
"curiosity": "Curiosité",
"charisma": "Charisme",
"psyche": "Psyché",
};
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
thunder: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Foudre' },
earth: { class: 'text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange', text: 'Terre' },
arcana: { class: 'text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo', text: 'Arcane' },
air: { class: 'text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime', text: 'Air' },
nature: { class: 'text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green', text: 'Nature' },
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
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 spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
export const CharacterValidation = z.object({
id: z.number(),
name: z.string(),
people: z.number().nullable(),
level: z.number().min(1).max(20),
aspect: z.number().nullable().optional(),
notes: z.string().nullable().optional(),
health: z.number().default(0),
mana: z.number().default(0),
training: z.object(MAIN_STATS.reduce((p, v) => {
p[v] = z.array(z.tuple([z.number().min(0).max(15), z.number()]));
return p;
}, {} as Record<MainStat, z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>)),
leveling: z.array(z.tuple([z.number().min(1).max(20), z.number()])),
abilities: z.object(ABILITIES.reduce((p, v) => {
p[v] = z.tuple([z.number(), z.number()]);
return p;
}, {} as Record<Ability, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>)).partial(),
spells: z.string().array(),
modifiers: z.object(MAIN_STATS.reduce((p, v) => {
p[v] = z.number();
return p;
}, {} as Record<MainStat, z.ZodNumber>)).partial(),
owner: z.number(),
username: z.string().optional(),
visibility: z.enum(["public", "private"]),
thumbnail: z.any(),
});
const stepTexts: Record<number, string> = {
0: 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.',
1: 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.',
2: 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.',
3: 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.',
4: 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.'
};
type PropertySum = { list: Array<string | number>, value: number, _dirty: boolean };
export class CharacterBuilder
{
private _container: HTMLDivElement;
private _content?: HTMLDivElement;
private _stepsHeader: HTMLDivElement[] = [];
private _stepsContent: BuilderTab[] = [];
private id?: string;
private _character!: Character;
private _result!: CompiledCharacter;
private _buffer: Record<string, PropertySum> = {};
constructor(container: HTMLDivElement, id?: string)
{
this.id = id;
this._container = container;
if(id)
{
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
container.replaceChildren(load);
useRequestFetch()(`/api/character/${id}`).then(character => {
if(character)
{
this._character = character;
document.title = `d[any] - Edition de ${character.name ?? 'nouveau personnage'}`;
if(character.people)
{
const people = config.peoples[character.people]!;
character.leveling.forEach(e => {
const feature = people.options[e[0]][e[1]]!;
feature.effect.map(e => this.apply(e));
});
MAIN_STATS.forEach(stat => {
character.training[stat].forEach(option => {
config.training[stat][option[0]][option[1]]!.features?.forEach(this.apply.bind(this));
})
});
}
load.remove();
this.render();
this.display(0);
}
});
}
else
{
this._character = Object.assign({}, defaultCharacter);
document.title = `d[any] - Edition de nouveau personnage`;
this.render();
this.display(0);
}
}
private render()
{
this._stepsHeader = [
dom("div", { class: "group flex items-center", }, [
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(0) } }, [text("Peuples")]),
]),
dom("div", { class: "group flex items-center", }, [
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(1) } }, [text("Niveaux")]),
]),
dom("div", { class: "group flex items-center", }, [
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(2) } }, [text("Entrainement")]),
]),
dom("div", { class: "group flex items-center", }, [
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(3) } }, [text("Compétences")]),
]),
dom("div", { class: "group flex items-center", }, [
icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }),
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: e => this.display(4) } }, [text("Aspect")])
]),
];
this._stepsContent = [
new PeoplePicker(this),
new LevelPicker(this),
];
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [
div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [popper(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), {
arrow: true,
offset: 8,
content: [ text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.") ],
placement: "bottom-end",
class: "max-w-96 fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50"
})]),
]),
this._content,
]));
}
display(step: number)
{
if(step < 0 || step >= this._stepsHeader.length)
return;
if(this._stepsContent.slice(0, step).some(e => !e.validate()))
return;
this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
this._stepsHeader[step]!.setAttribute('data-state', 'active');
this._stepsContent[step]!.update();
this._content?.replaceChildren(...this._stepsContent[step]!.dom);
}
async save(leave: boolean = true)
{
if(this.id === 'new')
{
//@ts-ignore
this.id = this._character.id = await useRequestFetch()(`/api/character`, {
method: 'post',
body: this._character,
onResponseError: (e) => {
//add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
}
});
//add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
useRouter().replace({ name: 'character-id-edit', params: { id: this.id } })
if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
}
else
{
//@ts-ignore
await useRequestFetch()(`/api/character/${id}`, {
method: 'post',
body: this._character,
onResponseError: (e) => {
//add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
}
});
//add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
}
}
get character(): Character
{
return this._character;
}
get compiled(): CompiledCharacter
{
this.compile(Object.keys(this._buffer));
return this._result;
}
get values(): Record<string, number>
{
const keys = Object.keys(this._buffer);
this.compile(keys);
return keys.reduce((p, v) => {
p[v] = this._buffer[v]!.value;
return p;
}, {} as Record<string, number>);
}
private compile(properties: string[])
{
const queue = properties;
queue.forEach(property => {
const buffer = this._buffer[property];
if(property === "")
return
if(buffer && buffer._dirty === true)
{
let sum = 0;
for(let i = 0; i < buffer.list.length; i++)
{
if(typeof buffer.list[i] === 'string')
{
if(this._buffer[buffer.list[i]!]!._dirty)
{
//Put it back in queue since its dependencies haven't been resolved yet
queue.push(property);
return;
}
else
sum += this._buffer[buffer.list[i]!]!.value;
}
else
sum += buffer.list[i] as number;
}
const path = property.split("/");
const object = path.slice(0, -1).reduce((p, v) => p[v], this._result as any);
object[path.slice(-1)[0]!] = sum;
this._buffer[property]!.value = sum;
this._buffer[property]!._dirty = false;
}
})
}
updateLevel(level: Level)
{
this._character.level = level;
if(this._character.leveling) //Invalidate higher levels
{
for(let level = 20; level > this._character.level; level--)
{
const index = this._character.leveling.findIndex(e => e[0] == level);
if(index !== -1)
{
const option = this._character.leveling[level]!;
this._character.leveling.splice(index, 1);
this.remove(config.peoples[this._character.people!]!.options[option[0]][option[1]]!);
}
}
}
}
toggleLevelOption(level: Level, choice: number)
{
if(level > this._character.level) //Cannot add more level options than the current level
return;
if(this._character.leveling === undefined) //Add level 1 if missing
{
this._character.leveling = [[1, 0]];
this.add(config.peoples[this._character.people!]!.options[1][0]!);
}
if(level == 1) //Cannot remove level 1
return;
for(let i = 1; i < level; i++) //Check previous levels as a requirement
{
if(!this._character.leveling.some(e => e[0] == i))
return;
}
const option = this._character.leveling.find(e => e[0] == level);
if(option && option[1] !== choice) //If the given level is already selected, switch to the new choice
{
this._character.leveling.splice(this._character.leveling.findIndex(e => e[0] == level), 1, [level, choice]);
this.remove(config.peoples[this._character.people!]!.options[option[0]][option[1]]!);
this.add(config.peoples[this._character.people!]!.options[level][choice]!);
}
else if(!option)
{
this._character.leveling.push([level, choice]);
this.add(config.peoples[this._character.people!]!.options[level][choice]!);
}
}
toggleTrainingOption(stat: MainStat, level: TrainingLevel, option: number)
{
}
private add(feature: Feature)
{
feature.effect.forEach(this.apply.bind(this));
}
private remove(feature: Feature)
{
}
private apply(feature: FeatureItem)
{
switch(feature.category)
{
case "feature":
this._result.features[feature.kind].push(feature.text);
return;
case "list":
if(feature.action === 'add' && !this._result[feature.list].includes(feature.item))
this._result[feature.list].push(feature.item);
else
this._result[feature.list] = this._result[feature.list].filter((e: string) => e !== feature.item);
return;
case "value":
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
if(feature.operation === 'add')
this._buffer[feature.property]!.list.push(feature.value);
else if(feature.operation === 'set')
this._buffer[feature.property]!.list = [feature.value];
this._buffer[feature.property]!._dirty = true;
return;
case "choice":
const choice = this._character.choices[feature.id]!;
choice.forEach(e => this.apply(feature.options[e]!));
return;
default:
return;
}
}
}
interface BuilderTab {
dom: Array<Node | string>;
update: () => void;
validate: () => boolean;
};
class PeoplePicker implements BuilderTab
{
private _builder: CharacterBuilder;
private _content: Array<Node | string>;
private _nameInput: HTMLInputElement;
private _visibilityInput: HTMLDivElement;
private _options: HTMLDivElement[];
private _activeOption?: HTMLDivElement;
constructor(builder: CharacterBuilder)
{
this._builder = builder;
this._nameInput = dom("input", { class: `mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, listeners: {
input: (e: Event) => {
this._builder.character.name = this._nameInput.value ?? '';
document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`;
}
}});
this._visibilityInput = dom("div", { class: `group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, attributes: { "data-state": "unckecked" }, listeners: {
click: (e: Event) => {
this._builder.character.visibility = this._builder.character.visibility === "private" ? "public" : "private";
console.log(this._builder.character.visibility);
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
}
}}, [ 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') ]);
this._options = config.peoples.map(
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
this._builder.character.people = i;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
this._activeOption = this._options[i]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
}
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
);
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
dom("label", { class: "flex justify-center items-center my-2" }, [
dom("span", { class: "pb-1 md:p-0", text: "Nom" }),
this._nameInput,
]),
dom("label", { class: "flex justify-center items-center my-2" }, [
dom("span", { class: "md:text-base text-sm", text: "Privé ?" }),
this._visibilityInput,
]),
button(text('Suivant'), () => this._builder.display(1), 'h-[35px] px-[15px]'),
]), div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options)];
}
update()
{
this._nameInput.value = this._builder.character.name;
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
if(this._builder.character.people !== undefined)
{
this._activeOption = this._options[this._builder.character.people]!;
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
}
}
validate(): boolean
{
return this._builder.character.people !== undefined;
}
get dom()
{
return this._content;
}
}
class LevelPicker implements BuilderTab
{
private _builder: CharacterBuilder;
private _content: Array<Node | string>;
private _levelInput: HTMLInputElement;
private _pointsInput: HTMLInputElement;
private _healthText: Text;
private _manaText: Text;
private _options: HTMLDivElement[][];
constructor(builder: CharacterBuilder)
{
this._builder = builder;
this._levelInput = dom("input", { class: `w-14 mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, listeners: {
input: (e: Event) => {
this._builder.character.level = parseInt(this._levelInput.value) ?? 1;
this.updateLevel();
},
keydown: (e: KeyboardEvent) => {
let value = this._levelInput.value;
switch(e.key)
{
case "ArrowUp":
value = clamp(parseInt(value) + 1, 1, 20).toString();
break;
case "ArrowDown":
value = clamp(parseInt(value) - 1, 1, 20).toString();
break;
default:
break;
}
if(this._levelInput.value !== value)
{
this._levelInput.value = value;
this._builder.character.level = parseInt(value);
this.updateLevel();
}
}
}});
this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
this._healthText = text("0"), this._manaText = text("0");
this._options = Object.entries(config.peoples[this._builder.character.people!]!.options).map(
(level) => [ div("w-full h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), div("sticky top-0", [ text(level[0]) ])]),
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => dom("div", { class: ["flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px]", { 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': (level[0] as any as Level) <= this._builder.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': this._builder.character.leveling?.some(e => e[0] == (level[0] as any as Level) && e[1] === j) ?? false }], listeners: { click: e => {
this._builder.toggleLevelOption(level[0] as any as Level, j);
}}}, [ dom('span', { class: "text-wrap whitespace-pre", text: option.description }) ])))
]);
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
dom("label", { class: "flex justify-center items-center my-2" }, [
dom("span", { class: "pb-1 md:p-0", text: "Niveau" }),
this._levelInput,
]),
dom("label", { class: "flex justify-center items-center my-2" }, [
dom("span", { class: "md:text-base text-sm", text: "Points restantes" }),
this._pointsInput,
]),
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
dom("span", { text: "Vie" }),
this._healthText,
]),
div("flex justify-center items-center gap-2 my-2 md:text-base text-sm", [
dom("span", { text: "Mana" }),
this._manaText,
]),
button(text('Suivant'), () => this._builder.display(1), 'h-[35px] px-[15px]'),
]), div('flex flex-col flex-1 gap-4 mx-8 my-4', this._options.flatMap(e => [...e]))];
}
update()
{
const values = this._builder.values;
this._levelInput.value = this._builder.character.level.toString();
this._pointsInput.value = (this._builder.character.level - this._builder.character.leveling.length).toString();
this._healthText.textContent = values.health?.toString() ?? '0';
this._manaText.textContent = values.mana?.toString() ?? '0';
this.updateLevel();
}
private updateLevel()
{
this._builder.updateLevel(this._builder.character.level as Level);
this._options.forEach((e, i) => {
e[0]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
e[1]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
e[1]?.childNodes.forEach((option, j) => {
'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level));
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, this._builder.character.leveling?.some(e => e[0] == ((i + 1) as Level) && e[1] === j) ?? false));
})
});
}
validate(): boolean
{
return this._builder.character.people !== undefined;
}
get dom()
{
return this._content;
}
}