From 5387dc66c3a510e917a3d8e29a80143ca266de6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Tue, 26 Aug 2025 10:18:07 +0200 Subject: [PATCH] Character BuilderTabs rework --- shared/character.util.ts | 191 ++++++++++++++++----------------------- 1 file changed, 78 insertions(+), 113 deletions(-) diff --git a/shared/character.util.ts b/shared/character.util.ts index ad054b6..fefac14 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -192,14 +192,6 @@ export const CharacterValidation = z.object({ thumbnail: z.any(), }); -const stepTexts: Record = { - 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 Property = { value: number | string | false, id: string, operation: "set" | "add" }; type PropertySum = { list: Array, value: number, _dirty: boolean }; export class CharacterCompiler @@ -400,7 +392,7 @@ export class CharacterBuilder extends CharacterCompiler private _container: HTMLDivElement; private _content?: HTMLDivElement; private _stepsHeader: HTMLDivElement[] = []; - private _stepsContent: Array BuilderTab)> = []; + private _steps: Array = []; private _helperText!: Text; private id?: string; @@ -437,34 +429,18 @@ export class CharacterBuilder extends CharacterCompiler } 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), - () => new TrainingPicker(this), - () => new AbilityPicker(this), - () => new AspectPicker(this), + this._steps = [ + PeoplePicker, + LevelPicker, + TrainingPicker, + AbilityPicker, + AspectPicker, ]; + this._stepsHeader = this._steps.map((e, i) => + 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: () => this.display(i) } }, [text(e.name)]), + ]) + ); this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.") this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } }); this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [ @@ -485,18 +461,15 @@ export class CharacterBuilder extends CharacterCompiler if(step < 0 || step >= this._stepsHeader.length) return; - if(step !== 0 && this._stepsContent.slice(0, step).some(e => !(e as BuilderTab).validate())) + if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this))) return; - this._stepsContent.forEach((e, i, arr) => arr[i] = i <= step ? typeof e === 'function' ? e() : e : e); - this._stepsHeader.forEach(e => e.setAttribute('data-state', 'inactive')); this._stepsHeader[step]!.setAttribute('data-state', 'active'); - (this._stepsContent[step]! as BuilderTab).update(); - this._content?.replaceChildren(...(this._stepsContent[step] as BuilderTab)!.dom); + this._content?.replaceChildren(...(new this._steps[step]!(this)).dom); - this._helperText.textContent = stepTexts[step]!; + this._helperText.textContent = this._steps[step]!.description; } async save(leave: boolean = true) { @@ -696,25 +669,37 @@ export class PickableFeature return this._content; } } -interface BuilderTab { - dom: Array; - update: () => void; - validate: () => boolean; -}; -class PeoplePicker implements BuilderTab -{ - private _builder: CharacterBuilder; - private _content: Array; +abstract class BuilderTab { + protected _builder: CharacterBuilder; + protected _content!: Array; + static name: string; + static description: string; + constructor(builder: CharacterBuilder) { this._builder = builder; } + update() { } + static validate(builder: CharacterBuilder): boolean { return false; } + get dom() { return this._content; } +}; +type BuilderTabConstructor = { + new (builder: CharacterBuilder): BuilderTab; + name: string; + description: string; + validate(builder: CharacterBuilder): boolean; +} +class PeoplePicker extends BuilderTab +{ private _nameInput: HTMLInputElement; private _visibilityInput: HTMLDivElement; private _options: HTMLDivElement[]; private _activeOption?: HTMLDivElement; + static override name = 'Peuple'; + static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.'; + constructor(builder: CharacterBuilder) { - this._builder = builder; + super(builder); this._nameInput = input('text', { input: (value) => { @@ -746,7 +731,7 @@ class PeoplePicker implements BuilderTab 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() + override update() { this._nameInput.value = this._builder.character.name; this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked"); @@ -758,29 +743,25 @@ class PeoplePicker implements BuilderTab "border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true)); } } - validate(): boolean + static override validate(builder: CharacterBuilder): boolean { - return this._builder.character.people !== undefined; - } - get dom() - { - return this._content; + return builder.character.people !== undefined; } } -class LevelPicker implements BuilderTab +class LevelPicker extends BuilderTab { - private _builder: CharacterBuilder; - private _content: Array; - private _levelInput: HTMLInputElement; private _pointsInput: HTMLInputElement; private _healthText: Text; private _manaText: Text; private _options: HTMLDivElement[][]; + static override name = 'Niveaux'; + static override description = 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.'; + constructor(builder: CharacterBuilder) { - this._builder = builder; + super(builder); this._levelInput = numberpicker({ defaultValue: this._builder.character.level, min: 1, max: 20, input: (value) => { this._builder.character.level = value; @@ -814,7 +795,7 @@ class LevelPicker implements BuilderTab button(text('Suivant'), () => this._builder.display(2), 'h-[35px] px-[15px]'), ]), div('flex flex-col flex-1 gap-4 mx-8 my-4', this._options.flatMap(e => [...e]))]; } - update() + override update() { const values = this._builder.values; @@ -839,20 +820,13 @@ class LevelPicker implements BuilderTab }); }); */ } - validate(): boolean + static override validate(builder: CharacterBuilder): boolean { - return this._builder.character.level - Object.keys(this._builder.character.leveling).length >= 0; - } - get dom() - { - return this._content; + return builder.character.level - Object.keys(builder.character.leveling).length >= 0; } } -class TrainingPicker implements BuilderTab +class TrainingPicker extends BuilderTab { - private _builder: CharacterBuilder; - private _content: Array; - private _pointsInput: HTMLInputElement; private _healthText: Text; private _manaText: Text; @@ -862,15 +836,18 @@ class TrainingPicker implements BuilderTab private _statIndicator: HTMLSpanElement; private _statContainer: HTMLDivElement; + static override name = 'Entrainement'; + static override description = 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.'; + constructor(builder: CharacterBuilder) { + super(builder); const statRenderBlock = (stat: MainStat) => { return Object.entries(config.training[stat]).map( (level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]), div("flex flex-row gap-4 justify-center", level[1].map((option, j) => new PickableFeature(option, { state: level[0] == '0' || this._builder.character.training[stat as MainStat][level[0] as any as TrainingLevel] === j, choices: this._builder.character.choices }).dom)) ]) } - this._builder = builder; 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 ps-3 pe-1 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"); @@ -912,7 +889,7 @@ class TrainingPicker implements BuilderTab this._statContainer.style.left = `-${tab * 100}%`; } - update() + override update() { const values = this._builder.values; const training = Object.values(this._builder.character.training).reduce((p, v) => p + Object.values(v).filter(e => e !== undefined).length, 0); @@ -921,31 +898,28 @@ class TrainingPicker implements BuilderTab this._healthText.textContent = values.health?.toString() ?? '0'; this._manaText.textContent = values.mana?.toString() ?? '0'; } - validate(): boolean + static override validate(builder: CharacterBuilder): boolean { - const values = this._builder.values; - const training = Object.values(this._builder.character.training).reduce((p, v) => p + Object.values(v).filter(e => e !== undefined).length, 0); + const values = builder.values; + const training = Object.values(builder.character.training).reduce((p, v) => p + Object.values(v).filter(e => e !== undefined).length, 0); return (values.training ?? 0) - training >= 0; } - get dom() - { - return this._content; - } } -class AbilityPicker implements BuilderTab +class AbilityPicker extends BuilderTab { - private _builder: CharacterBuilder; - private _content: Array; - private _pointsInput: HTMLInputElement; private _options: HTMLDivElement[]; private _tooltips: Text[] = []; private _maxs: HTMLElement[] = []; + static override name = 'Compétences'; + static override description = 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.'; + constructor(builder: CharacterBuilder) { + super(builder); const numberInput = (value?: number, update?: (value: number) => number | undefined) => { const input = 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) => { @@ -980,7 +954,6 @@ class AbilityPicker implements BuilderTab arr.push(value); return value; } - this._builder = builder; 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 ps-3 pe-1 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 }}); @@ -1017,7 +990,7 @@ class AbilityPicker implements BuilderTab button(text('Suivant'), () => this._builder.display(4), 'h-[35px] px-[15px]'), ]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48', this._options)]; } - update() + override update() { const values = this._builder.values; const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0); @@ -1034,23 +1007,16 @@ class AbilityPicker implements BuilderTab return this._builder.character.abilities[e]; }) } - validate(): boolean + static override validate(builder: CharacterBuilder): boolean { - const values = this._builder.values; - const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0); + const values = builder.values; + const abilities = Object.values(builder.character.abilities).reduce((p, v) => p + v, 0); return (values.ability ?? 0) - abilities >= 0; } - get dom() - { - return this._content; - } } -class AspectPicker implements BuilderTab +class AspectPicker extends BuilderTab { - private _builder: CharacterBuilder; - private _content: Array; - private _physicInput: HTMLInputElement; private _mentalInput: HTMLInputElement; private _personalityInput: HTMLInputElement; @@ -1059,9 +1025,12 @@ class AspectPicker implements BuilderTab private _options: HTMLDivElement[]; + static override name = 'Aspect'; + static override description = 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.'; + constructor(builder: CharacterBuilder) { - this._builder = builder; + super(builder); this._physicInput = 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 ps-3 pe-1 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._mentalInput = 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 ps-3 pe-1 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 }}); @@ -1119,7 +1088,7 @@ class AspectPicker implements BuilderTab button(text('Enregistrer'), () => this._builder.save(), 'h-[35px] px-[15px]'), ]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', this._options)]; } - update() + override update() { const physic = Object.values(this._builder.character.training['strength']).length + Object.values(this._builder.character.training['dexterity']).length + Object.values(this._builder.character.training['constitution']).length; const mental = Object.values(this._builder.character.training['intelligence']).length + Object.values(this._builder.character.training['curiosity']).length; @@ -1148,16 +1117,16 @@ class AspectPicker implements BuilderTab return true; })); } - validate(): boolean + static override validate(builder: CharacterBuilder): boolean { - const physic = Object.values(this._builder.character.training['strength']).length + Object.values(this._builder.character.training['dexterity']).length + Object.values(this._builder.character.training['constitution']).length; - const mental = Object.values(this._builder.character.training['intelligence']).length + Object.values(this._builder.character.training['curiosity']).length; - const personality = Object.values(this._builder.character.training['charisma']).length + Object.values(this._builder.character.training['psyche']).length; + const physic = Object.values(builder.character.training['strength']).length + Object.values(builder.character.training['dexterity']).length + Object.values(builder.character.training['constitution']).length; + const mental = Object.values(builder.character.training['intelligence']).length + Object.values(builder.character.training['curiosity']).length; + const personality = Object.values(builder.character.training['charisma']).length + Object.values(builder.character.training['psyche']).length; - if(this._builder.character.aspect === undefined) + if(builder.character.aspect === undefined) return false; - const aspect = config.aspects[this._builder.character.aspect]! + const aspect = config.aspects[builder.character.aspect]! if(physic > aspect.physic.max || physic < aspect.physic.min) return false; @@ -1168,8 +1137,4 @@ class AspectPicker implements BuilderTab return true; } - get dom() - { - return this._content; - } } \ No newline at end of file