Add Trees and Masteries in the feature editor. Add some items.
This commit is contained in:
parent
796b335b2e
commit
ce3dbb0d6e
|
|
@ -61,8 +61,7 @@ export type TreeStructure = {
|
||||||
name: string;
|
name: string;
|
||||||
nodes: FeatureID[];
|
nodes: FeatureID[];
|
||||||
|
|
||||||
// { 'from_id': { 'pathname': 'to_id' } };
|
paths: Record<number, number | number[]>;
|
||||||
paths: Record<number, Record<string, number>>;
|
|
||||||
};
|
};
|
||||||
type CommonState = {
|
type CommonState = {
|
||||||
capacity?: number;
|
capacity?: number;
|
||||||
|
|
@ -199,16 +198,22 @@ export type FeatureEquipment = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
category: "value";
|
category: "value";
|
||||||
operation: "add" | "set" | "min";
|
operation: "add" | "set" | "min";
|
||||||
property: `item/${RecursiveKeyOf<(ArmorState | WeaponState | WondrousState | MundaneState) & CommonState>}`;
|
property: `item/${RecursiveKeyOf<ArmorState & WeaponState & WondrousState & MundaneState & CommonState>}`;
|
||||||
value: number | `modifier/${MainStat}` | false;
|
value: number | `modifier/${MainStat}` | false;
|
||||||
}
|
};
|
||||||
export type FeatureList = {
|
export type FeatureList = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
category: "list";
|
category: "list";
|
||||||
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
|
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive" | "mastery";
|
||||||
action: "add" | "remove";
|
action: "add" | "remove";
|
||||||
item: string;
|
item: string;
|
||||||
};
|
};
|
||||||
|
export type FeatureTree = {
|
||||||
|
id: FeatureID;
|
||||||
|
category: "tree";
|
||||||
|
tree: string;
|
||||||
|
option?: number;
|
||||||
|
};
|
||||||
export type FeatureChoice = {
|
export type FeatureChoice = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
category: "choice";
|
category: "choice";
|
||||||
|
|
@ -217,9 +222,9 @@ export type FeatureChoice = {
|
||||||
amount: number;
|
amount: number;
|
||||||
exclusive: boolean; //Disallow to pick the same option twice
|
exclusive: boolean; //Disallow to pick the same option twice
|
||||||
};
|
};
|
||||||
options: Array<{ text: string, effects: Array<FeatureValue | FeatureList> }>; //TODO -> TextID
|
options: Array<{ text: string, effects: Array<FeatureValue | FeatureList | FeatureTree> }>; //TODO -> TextID
|
||||||
};
|
};
|
||||||
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice;
|
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice | FeatureTree;
|
||||||
export type Feature = {
|
export type Feature = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
description: string; //TODO -> TextID
|
description: string; //TODO -> TextID
|
||||||
|
|
@ -246,6 +251,7 @@ export type CompiledCharacter = {
|
||||||
|
|
||||||
bonus?: Partial<CompiledCharacter['bonus']>;
|
bonus?: Partial<CompiledCharacter['bonus']>;
|
||||||
};
|
};
|
||||||
|
mastery: Array<`weapon/${WeaponType}` | `armor/${'light' | 'medium' | 'heavy'}`>;
|
||||||
speed: number | false;
|
speed: number | false;
|
||||||
capacity: number | false;
|
capacity: number | false;
|
||||||
initiative: number;
|
initiative: number;
|
||||||
|
|
@ -263,18 +269,6 @@ export type CompiledCharacter = {
|
||||||
passivedodge: number;
|
passivedodge: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
mastery: {
|
|
||||||
strength: number;
|
|
||||||
dexterity: number;
|
|
||||||
shield: number;
|
|
||||||
armor: number;
|
|
||||||
multiattack: number;
|
|
||||||
magicpower: number;
|
|
||||||
magicspeed: number;
|
|
||||||
magicelement: number;
|
|
||||||
magicinstinct: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
bonus: {
|
bonus: {
|
||||||
defense: Partial<Record<MainStat, number>>; //Defense aux jets de resistance
|
defense: Partial<Record<MainStat, number>>; //Defense aux jets de resistance
|
||||||
abilities: Partial<Record<Ability, number>>;
|
abilities: Partial<Record<Ability, number>>;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,8 +1,8 @@
|
||||||
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, EnchantementConfig, FeatureEquipment, FeatureItem, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
|
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CompiledCharacter, DamageType, EnchantementConfig, FeatureEquipment, FeatureID, FeatureItem, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import characterConfig from '#shared/character-config.json';
|
import characterConfig from '#shared/character-config.json';
|
||||||
import proses, { a, preview } from "#shared/proses";
|
import proses, { preview } from "#shared/proses";
|
||||||
import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, optionmenu, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
import { button, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
||||||
import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom.util";
|
import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom.util";
|
||||||
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||||
import { clamp } from "#shared/general.util";
|
import { clamp } from "#shared/general.util";
|
||||||
|
|
@ -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, reactivity } from '#shared/reactive';
|
import { raw, reactive } from '#shared/reactive';
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","na
|
||||||
export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const;
|
export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const;
|
||||||
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", "improvised"] as const;
|
||||||
|
|
||||||
export const defaultCharacter: Character = {
|
export const defaultCharacter: Character = {
|
||||||
id: -1,
|
id: -1,
|
||||||
|
|
@ -34,7 +34,7 @@ export const defaultCharacter: Character = {
|
||||||
people: undefined,
|
people: undefined,
|
||||||
level: 1,
|
level: 1,
|
||||||
|
|
||||||
training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 }; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0 }; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
|
||||||
leveling: { 1: 0 },
|
leveling: { 1: 0 },
|
||||||
abilities: {},
|
abilities: {},
|
||||||
choices: {},
|
choices: {},
|
||||||
|
|
@ -107,17 +107,7 @@ const defaultCompiledCharacter = (character: Character) => ({
|
||||||
passiveparry: 0,
|
passiveparry: 0,
|
||||||
passivedodge: 0,
|
passivedodge: 0,
|
||||||
},
|
},
|
||||||
mastery: {
|
mastery: [],
|
||||||
strength: 0,
|
|
||||||
dexterity: 0,
|
|
||||||
shield: 0,
|
|
||||||
armor: 0,
|
|
||||||
multiattack: 1,
|
|
||||||
magicpower: 0,
|
|
||||||
magicspeed: 0,
|
|
||||||
magicelement: 0,
|
|
||||||
magicinstinct: 0,
|
|
||||||
},
|
|
||||||
bonus: {
|
bonus: {
|
||||||
abilities: {},
|
abilities: {},
|
||||||
defense: {},
|
defense: {},
|
||||||
|
|
@ -334,7 +324,8 @@ export class CharacterCompiler
|
||||||
Object.entries(value.training[stat]).forEach(option => this.add(config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]))
|
Object.entries(value.training[stat]).forEach(option => this.add(config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]))
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.entries(value.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
|
Object.entries(value.abilities).forEach(e => this._buffer[`abilities/${e[0]}`] = { value: 0, _dirty: true, min: -Infinity, list: [{ id: '', operation: 'add', value: e[1] }] })
|
||||||
|
//Object.entries(value.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get character(): Character
|
get character(): Character
|
||||||
|
|
@ -397,21 +388,21 @@ export class CharacterCompiler
|
||||||
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 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
protected add(feature?: string)
|
protected add(feature?: FeatureID)
|
||||||
{
|
{
|
||||||
if(!feature)
|
if(!feature)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
config.features[feature]?.effect.forEach((effect) => this.apply(effect));
|
config.features[feature]?.effect.forEach((effect) => this.apply(effect));
|
||||||
}
|
}
|
||||||
protected remove(feature?: string)
|
protected remove(feature?: FeatureID)
|
||||||
{
|
{
|
||||||
if(!feature)
|
if(!feature)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
config.features[feature]?.effect.forEach((effect) => this.undo(effect));
|
config.features[feature]?.effect.forEach((effect) => this.undo(effect));
|
||||||
}
|
}
|
||||||
protected apply(feature?: FeatureItem | FeatureEquipment)
|
protected apply(feature?: FeatureItem)
|
||||||
{
|
{
|
||||||
if(!feature)
|
if(!feature)
|
||||||
return;
|
return;
|
||||||
|
|
@ -449,7 +440,7 @@ export class CharacterCompiler
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected undo(feature?: FeatureItem | FeatureEquipment)
|
protected undo(feature?: FeatureItem)
|
||||||
{
|
{
|
||||||
if(!feature)
|
if(!feature)
|
||||||
return;
|
return;
|
||||||
|
|
@ -542,11 +533,7 @@ export class CharacterCompiler
|
||||||
if(stop === true)
|
if(stop === true)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const path = property.split("/");
|
setProperty(this._result, property, Math.max(sum, this._buffer[property]!.min));
|
||||||
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]!))
|
|
||||||
object[path.slice(-1)[0]!] = Math.max(sum, this._buffer[property]!.min);
|
|
||||||
|
|
||||||
this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min);
|
this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min);
|
||||||
this._buffer[property]!._dirty = false;
|
this._buffer[property]!._dirty = false;
|
||||||
|
|
@ -554,6 +541,14 @@ export class CharacterCompiler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function setProperty<T>(root: any, path: string, value: T | ((old: T) => T))
|
||||||
|
{
|
||||||
|
const arr = path.split("/"); //Get the property path as an array
|
||||||
|
const object = arr.length === 1 ? root : arr.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, root); //Get into the second to last property using the property path
|
||||||
|
|
||||||
|
if(object.hasOwnProperty(arr.slice(-1)[0]!))
|
||||||
|
object[arr.slice(-1)[0]!] = typeof value === 'function' ? (value as (old: T) => T)(object[arr.slice(-1)[0]!]) : value;
|
||||||
|
}
|
||||||
export class CharacterBuilder extends CharacterCompiler
|
export class CharacterBuilder extends CharacterCompiler
|
||||||
{
|
{
|
||||||
private _container: RedrawableHTML;
|
private _container: RedrawableHTML;
|
||||||
|
|
@ -1253,6 +1248,22 @@ class AspectPicker extends BuilderTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const masteryTexts: Record<CompiledCharacter['mastery'][number], { text: string, href: string }> = {
|
||||||
|
"armor/light": { text: "Armure légère", href: "regles/annexes/equipement#Les armures légères" },
|
||||||
|
"armor/medium": { text: "Armure moyenne", href: "regles/annexes/equipement#Les armures" },
|
||||||
|
"armor/heavy": { text: "Armure lourde", href: "regles/annexes/equipement#Les armures lourdes" },
|
||||||
|
"weapon/light": { text: "Arme légère", href: "regles/annexes/equipement#Les armes légères" },
|
||||||
|
"weapon/throw": { text: "Arme de jet", href: "regles/annexes/equipement#Les armes de jet" },
|
||||||
|
"weapon/natural": { text: "Arme naturelle", href: "regles/annexes/equipement#Les armes naturelles" },
|
||||||
|
"weapon/classic": { text: "Arme standard", href: "regles/annexes/equipement#Les armes" },
|
||||||
|
"weapon/improvised": { text: "Arme improvisée", href: "regles/annexes/equipement#Les armes improvisées" },
|
||||||
|
"weapon/heavy": { text: "Arme lourde", href: "regles/annexes/equipement#Les armes lourdes" },
|
||||||
|
"weapon/twohanded": { text: "Arme à deux mains", href: "regles/annexes/equipement#Les armes à deux mains" },
|
||||||
|
"weapon/finesse": { text: "Arme maniable", href: "regles/annexes/equipement#Les armes maniables" },
|
||||||
|
"weapon/projectile": { text: "Arme à projectiles", href: "regles/annexes/equipement#Les armes à projectiles" },
|
||||||
|
"weapon/reach": { text: "Arme longue", href: "regles/annexes/equipement#Les armes longues" },
|
||||||
|
"weapon/shield": { text: "Bouclier", href: "regles/annexes/equipement#Les boucliers" },
|
||||||
|
}
|
||||||
type Category = ItemConfig['category'];
|
type Category = ItemConfig['category'];
|
||||||
type Rarity = ItemConfig['rarity'];
|
type Rarity = ItemConfig['rarity'];
|
||||||
export const colorByRarity: Record<Rarity, string> = {
|
export const colorByRarity: Record<Rarity, string> = {
|
||||||
|
|
@ -1272,6 +1283,7 @@ export const weaponTypeTexts: Record<WeaponType, string> = {
|
||||||
"finesse": 'maniable',
|
"finesse": 'maniable',
|
||||||
"reach": 'longue',
|
"reach": 'longue',
|
||||||
"projectile": 'à projectile',
|
"projectile": 'à projectile',
|
||||||
|
"improvised": "improvisée"
|
||||||
}
|
}
|
||||||
export const armorTypeTexts: Record<ArmorConfig["type"], string> = {
|
export const armorTypeTexts: Record<ArmorConfig["type"], string> = {
|
||||||
'heavy': 'Armure lourde',
|
'heavy': 'Armure lourde',
|
||||||
|
|
@ -1594,7 +1606,8 @@ export class CharacterSheet
|
||||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
||||||
]),
|
]),
|
||||||
|
|
||||||
() => character.mastery.strength + character.mastery.dexterity > 0 ? 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", { list: character.mastery, render: (e, _c) => 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', }) }),
|
||||||
|
/* () => 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 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 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 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,
|
||||||
|
|
@ -1613,7 +1626,7 @@ export class CharacterSheet
|
||||||
() => 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 > 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 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 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 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,
|
||||||
|
|
@ -2041,9 +2054,14 @@ export class CharacterSheet
|
||||||
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 gap-4', [
|
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(() => current.item?.enchantments?.includes(enchant.id) ? 'radix-icons:minus' : 'radix-icons:plus', { width: 16, height: 16 }), () => {
|
||||||
current.item!.enchantments?.push(enchant.id);
|
const idx = current.item!.enchantments?.findIndex(e => e === enchant.id) ?? -1;
|
||||||
// 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; }, {}));
|
if(idx === -1)
|
||||||
|
current.item!.enchantments?.push(enchant.id);
|
||||||
|
else
|
||||||
|
current.item!.enchantments?.splice(idx, 1);
|
||||||
|
|
||||||
|
current.item!.state ??= {};
|
||||||
|
|
||||||
this.character?.saveVariables();
|
this.character?.saveVariables();
|
||||||
}, 'p-1 !border-solid !border-r'),
|
}, 'p-1 !border-solid !border-r'),
|
||||||
|
|
|
||||||
|
|
@ -344,7 +344,7 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
||||||
return;
|
return;
|
||||||
default: return;
|
default: return;
|
||||||
}
|
}
|
||||||
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
|
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20 w-full' });
|
||||||
|
|
||||||
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]) as HTMLLabelElement & { disabled: boolean, value: T | undefined };
|
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]) as HTMLLabelElement & { disabled: boolean, value: T | undefined };
|
||||||
let value: T | undefined = undefined;
|
let value: T | undefined = undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponType } from "~/types/character";
|
import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponType } from "~/types/character";
|
||||||
import { div, dom, icon, span, text, type NodeChildren, type RedrawableHTML } from "#shared/dom.util";
|
import { div, dom, icon, span, text, type NodeChildren, type RedrawableHTML } from "#shared/dom.util";
|
||||||
import { MarkdownEditor } from "#shared/editor.util";
|
import { MarkdownEditor } from "#shared/editor.util";
|
||||||
import { preview } from "#shared/proses";
|
import { preview } from "#shared/proses";
|
||||||
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
|
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
|
||||||
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
|
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||||
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, categoryText, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, weaponTypeTexts } from "#shared/character.util";
|
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, categoryText, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, masteryTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, weaponTypeTexts } from "#shared/character.util";
|
||||||
import characterConfig from "#shared/character-config.json";
|
import characterConfig from "#shared/character-config.json";
|
||||||
import { getID } from "#shared/general.util";
|
import { getID } from "#shared/general.util";
|
||||||
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
|
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
|
||||||
|
|
@ -491,7 +491,7 @@ export class HomebrewBuilder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeatureOption = Partial<FeatureValue | FeatureEquipment | FeatureList | FeatureChoice> & { id: string };
|
type FeatureOption = Partial<FeatureValue | FeatureEquipment | FeatureList | FeatureChoice | FeatureTree> & { id: string };
|
||||||
class FeatureEditor
|
class FeatureEditor
|
||||||
{
|
{
|
||||||
private _list: Record<string, FeatureOption> | FeatureOption[];
|
private _list: Record<string, FeatureOption> | FeatureOption[];
|
||||||
|
|
@ -576,11 +576,13 @@ class FeatureEditor
|
||||||
switch(effect.category)
|
switch(effect.category)
|
||||||
{
|
{
|
||||||
case 'value':
|
case 'value':
|
||||||
return flattenFeatureChoices.findLast(e => e.category === 'value' && e.property === effect.property);
|
return flattenFeatureChoices.find(e => e.category === 'value' && e.property === effect.property);
|
||||||
case 'choice':
|
case 'choice':
|
||||||
return flattenFeatureChoices.findLast(e => e.category === 'choice');
|
return flattenFeatureChoices.findLast(e => e.category === 'choice');
|
||||||
|
case 'tree':
|
||||||
|
return flattenFeatureChoices.findLast(e => e.category === 'tree');
|
||||||
case 'list':
|
case 'list':
|
||||||
return flattenFeatureChoices.findLast(e => e.category === 'list' && e.list === effect.list);
|
return flattenFeatureChoices.find(e => e.category === 'list' && e.list === effect.list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private editByCategory(buffer: FeatureOption)
|
private editByCategory(buffer: FeatureOption)
|
||||||
|
|
@ -594,6 +596,8 @@ class FeatureEditor
|
||||||
return this.editList(buffer as Partial<FeatureList>);
|
return this.editList(buffer as Partial<FeatureList>);
|
||||||
case 'choice':
|
case 'choice':
|
||||||
return this.editChoice(buffer as Partial<FeatureChoice>);
|
return this.editChoice(buffer as Partial<FeatureChoice>);
|
||||||
|
case 'tree':
|
||||||
|
return this.editTree(buffer as Partial<FeatureTree>);
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
return { top, bottom };
|
return { top, bottom };
|
||||||
|
|
@ -620,21 +624,20 @@ class FeatureEditor
|
||||||
}
|
}
|
||||||
private editList(buffer: Partial<FeatureList>)
|
private editList(buffer: Partial<FeatureList>)
|
||||||
{
|
{
|
||||||
let list: Option<string>[];
|
let list: Option<string>[] = [];
|
||||||
if(buffer.action === 'add')
|
switch(buffer.list)
|
||||||
{
|
{
|
||||||
if(buffer.list === 'spells')
|
case undefined:
|
||||||
{
|
break;
|
||||||
|
case "spells":
|
||||||
list = Object.values(config.spells).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(e.description)) ]) ]), value: e.id }));
|
list = Object.values(config.spells).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(e.description)) ]) ]), value: e.id }));
|
||||||
}
|
break;
|
||||||
else if(buffer.list)
|
case "mastery":
|
||||||
{
|
list = Object.entries(masteryTexts).map(e => ({ text: e[1].text, value: e[0] }));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
list = Object.values(config[buffer.list]).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id }));
|
list = Object.values(config[buffer.list]).map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id }));
|
||||||
}
|
break;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
list = (Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add') as FeatureList[]).map((e) => ({ text: config[e.list][e.item]!.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: config[e.list][e.item]!.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(config[e.list === 'sickness' ? 'features' : e.list][e.item]?.description))) ]) ]), value: e.item }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -642,7 +645,22 @@ class FeatureEditor
|
||||||
buffer.action = value as 'add' | 'remove';
|
buffer.action = value as 'add' | 'remove';
|
||||||
this.edit();
|
this.edit();
|
||||||
}, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ],
|
}, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ],
|
||||||
bottom: [ combobox(list!, { defaultValue: buffer.item, change: (item) => buffer.item = item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' }, fill: 'contain' }) ]
|
bottom: [ combobox(list, { defaultValue: buffer.item, change: (item) => buffer.item = item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' }, fill: 'contain' }) ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private editTree(buffer: Partial<FeatureTree>)
|
||||||
|
{
|
||||||
|
const edit = tooltip(button(icon('radix-icons:gear', { width: 16, height: 16 }), function() {
|
||||||
|
buffer.option = 0;
|
||||||
|
this.replaceWith(remove);
|
||||||
|
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Option fixe', 'right'), remove = tooltip(button(icon('radix-icons:cross-2', { width: 16, height: 16 }), function() {
|
||||||
|
buffer.option = undefined;
|
||||||
|
this.replaceWith(edit);
|
||||||
|
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Option fixe', 'right');
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: [ combobox(Object.values(config.trees).map(e => ({ text: e.name, value: e.name })), { defaultValue: buffer.tree, change: v => buffer.tree = v, class: { container: 'bg-light-25 dark:bg-dark-25 w-48 -m-px hover:z-10 h-[36px]' }, fill: 'cover' }), buffer.option === undefined ? edit : remove ],
|
||||||
|
bottom: [ ],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private editChoice(buffer: Partial<FeatureChoice>)
|
private editChoice(buffer: Partial<FeatureChoice>)
|
||||||
|
|
@ -709,7 +727,7 @@ class FeatureEditor
|
||||||
...top,
|
...top,
|
||||||
]),
|
]),
|
||||||
div('flex', [ tooltip(button(icon('radix-icons:check'), () => { this.update(); this.read(); this.show(); this._draft = false; }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), () => { if(this._draft) { this.delete(); this.container.remove(); } else { this.read(); this.show(); } }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ])
|
div('flex', [ tooltip(button(icon('radix-icons:check'), () => { this.update(); this.read(); this.show(); this._draft = false; }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), () => { if(this._draft) { this.delete(); this.container.remove(); } else { this.read(); this.show(); } }, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ])
|
||||||
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', bottom) ]);
|
]), bottom.length > 0 ? div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', bottom) : undefined ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = redraw();
|
let content = redraw();
|
||||||
|
|
@ -880,17 +898,8 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
|
||||||
{ text: 'Esquive active', value: { category: 'value', property: 'defense/activedodge', operation: 'add', value: 1 } },
|
{ text: 'Esquive active', value: { category: 'value', property: 'defense/activedodge', operation: 'add', value: 1 } },
|
||||||
{ text: 'Esquive passive', value: { category: 'value', property: 'defense/passivedodge', operation: 'add', value: 1 } }
|
{ text: 'Esquive passive', value: { category: 'value', property: 'defense/passivedodge', operation: 'add', value: 1 } }
|
||||||
] },
|
] },
|
||||||
{ text: 'Maitrise', value: [
|
{ text: 'Arbre', value: { category: 'tree', option: 0 } },
|
||||||
{ text: 'Maitrise des armes (for.)', value: { category: 'value', property: 'mastery/strength', operation: 'add', value: 1 } },
|
{ text: 'Maitrise', value: Object.keys(masteryTexts).map(e => ({ text: `Maitrise > ${masteryTexts[e as keyof typeof masteryTexts].text}`, value: { category: 'list', action: 'add', list: 'mastery', item: e } })) },
|
||||||
{ text: 'Maitrise des armes (dex.)', value: { category: 'value', property: 'mastery/dexterity', operation: 'add', value: 1 } },
|
|
||||||
{ text: 'Maitrise des boucliers', value: { category: 'value', property: 'mastery/shield', operation: 'add', value: 1 } },
|
|
||||||
{ text: 'Maitrise des armure', value: { category: 'value', property: 'mastery/armor', operation: 'add', value: 1 } },
|
|
||||||
{ text: 'Attaque multiple', value: { category: 'value', property: 'mastery/multiattack', operation: 'add', value: 1 } },
|
|
||||||
{ text: 'Arbre de magie (Puissance)', value: { category: 'value', property: 'mastery/magicpower', operation: 'add', value: 1 } },
|
|
||||||
{ text: 'Arbre de magie (Rapidité)', value: { category: 'value', property: 'mastery/magicspeed', operation: 'add', value: 1 } },
|
|
||||||
{ text: 'Arbre de magie (Elements)', value: { category: 'value', property: 'mastery/magicelement', operation: 'add', value: 1 } },
|
|
||||||
{ text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', operation: 'add', value: 1 } }
|
|
||||||
] },
|
|
||||||
{ text: 'Compétences', value: [
|
{ text: 'Compétences', value: [
|
||||||
...ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
|
...ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
|
||||||
{ text: 'Max de compétence', value: ABILITIES.map((e) => ({ text: `Max > ${abilityTexts[e as Ability]}`, value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
|
{ text: 'Max de compétence', value: ABILITIES.map((e) => ({ text: `Max > ${abilityTexts[e as Ability]}`, value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
|
||||||
|
|
@ -1114,6 +1123,10 @@ function textFromEffect(effect: Partial<FeatureOption>): string
|
||||||
{
|
{
|
||||||
return `${effect.text} (${effect.options?.length ?? 0} options).`;
|
return `${effect.text} (${effect.options?.length ?? 0} options).`;
|
||||||
}
|
}
|
||||||
|
else if(effect.category === 'tree')
|
||||||
|
{
|
||||||
|
return `Progression dans l'arbre ${effect.tree && config.trees[effect.tree] ? config.trees[effect.tree]?.name : "'Inconnu'"}`;
|
||||||
|
}
|
||||||
|
|
||||||
return `Inconnu`;
|
return `Inconnu`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue