Character BuilderTabs rework

This commit is contained in:
Clément Pons 2025-08-26 10:18:07 +02:00
parent 6fe3746df4
commit 5387dc66c3
1 changed files with 78 additions and 113 deletions

View File

@ -192,14 +192,6 @@ export const CharacterValidation = z.object({
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 Property = { value: number | string | false, id: string, operation: "set" | "add" };
type PropertySum = { list: Array<Property>, 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 | (() => BuilderTab)> = [];
private _steps: Array<BuilderTabConstructor> = [];
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<Node | string>;
update: () => void;
validate: () => boolean;
};
class PeoplePicker implements BuilderTab
{
private _builder: CharacterBuilder;
private _content: Array<Node | string>;
abstract class BuilderTab {
protected _builder: CharacterBuilder;
protected _content!: Array<Node | string>;
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<Node | string>;
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<Node | string>;
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<Node | string>;
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<Node | string>;
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;
}
}