From ce3dbb0d6ed3dbc1b0f2322bdb0057cfef2237e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Wed, 14 Jan 2026 22:40:58 +0100 Subject: [PATCH] Add Trees and Masteries in the feature editor. Add some items. --- app/types/character.d.ts | 32 +- db.sqlite | Bin 724992 -> 729088 bytes shared/character-config.json | 12106 +-------------------------------- shared/character.util.ts | 82 +- shared/components.util.ts | 2 +- shared/feature.util.ts | 73 +- 6 files changed, 108 insertions(+), 12187 deletions(-) diff --git a/app/types/character.d.ts b/app/types/character.d.ts index 65b607d..f931ac4 100644 --- a/app/types/character.d.ts +++ b/app/types/character.d.ts @@ -61,8 +61,7 @@ export type TreeStructure = { name: string; nodes: FeatureID[]; - // { 'from_id': { 'pathname': 'to_id' } }; - paths: Record>; + paths: Record; }; type CommonState = { capacity?: number; @@ -199,16 +198,22 @@ export type FeatureEquipment = { id: FeatureID; category: "value"; operation: "add" | "set" | "min"; - property: `item/${RecursiveKeyOf<(ArmorState | WeaponState | WondrousState | MundaneState) & CommonState>}`; + property: `item/${RecursiveKeyOf}`; value: number | `modifier/${MainStat}` | false; -} +}; export type FeatureList = { id: FeatureID; category: "list"; - list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive"; + list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive" | "mastery"; action: "add" | "remove"; item: string; }; +export type FeatureTree = { + id: FeatureID; + category: "tree"; + tree: string; + option?: number; +}; export type FeatureChoice = { id: FeatureID; category: "choice"; @@ -217,9 +222,9 @@ export type FeatureChoice = { amount: number; exclusive: boolean; //Disallow to pick the same option twice }; - options: Array<{ text: string, effects: Array }>; //TODO -> TextID + options: Array<{ text: string, effects: Array }>; //TODO -> TextID }; -export type FeatureItem = FeatureValue | FeatureList | FeatureChoice; +export type FeatureItem = FeatureValue | FeatureList | FeatureChoice | FeatureTree; export type Feature = { id: FeatureID; description: string; //TODO -> TextID @@ -246,6 +251,7 @@ export type CompiledCharacter = { bonus?: Partial; }; + mastery: Array<`weapon/${WeaponType}` | `armor/${'light' | 'medium' | 'heavy'}`>; speed: number | false; capacity: number | false; initiative: number; @@ -263,18 +269,6 @@ export type CompiledCharacter = { passivedodge: number; }; - mastery: { - strength: number; - dexterity: number; - shield: number; - armor: number; - multiattack: number; - magicpower: number; - magicspeed: number; - magicelement: number; - magicinstinct: number; - }; - bonus: { defense: Partial>; //Defense aux jets de resistance abilities: Partial>; diff --git a/db.sqlite b/db.sqlite index 85e7dd44e8591422ec07a3c00f918469b3078180..a7c363d9a7ce68dd283afb31476269278f0042f3 100644 GIT binary patch delta 6440 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 R0Y-9U2Yz-*oZ185`xlJws^kCw delta 1510 zcmYM!du&rx90%~8_P*NN$A}LIzsxX+SL-7IFX#X-Al?3nsnwTij=pROnf0(Edjeq&=vfxcV_nz~+zkANP zH|PEeAD9cX=3~oroI;_v3`;?E`~f@7DIS{s=W=XkST&gGEqxh&&{%1_UHGPks7kla z)>~CZ-IWvUO;^cfoYIx9e7D|>$ZeT5l1gY+r`J>J8WIzuVp@u)lI7bLqE{|fR4EjO zuMO`Qt}ALVRmJ_KMb;TZ=W->Z=IA94lT+I^CAgrcHyAMvrBX>TWBh2=I1m{f6>ABj zCUlKOiJV5&y)8ahr>iCC>C8rq1F2+EG{z#C%2kI^>-0Bdb_40cT5|gG4L@llsyh8O znGMXxQu^|>9mG!dBDHcTp%xZ{9c<7DO$&VouFc?t)vyNE!a7(F8=wJP;D#1x1rKZl zFZf^+Y=$k+279;S!8reF_LzGML&gU8_s*a!Qe2nXOG z9D>7e1ddjonnvSEcnXfead;X|z%%eHoCF!3gXiG|I0dI+2F}1)coAmd9J~ZC!z(Zc z=iya&173$W;bIj|Z^0#a4K8564Ds6i8lqEFtJji4t2sGB^R3!_&V|UqW%H3lzjt7? zFJ+H+1vcj0feu^1*5n!YMSXirX47z!-96OW(~HLL%ips6egCJ8Tdl$V9$QB$+S1{*wg(%sjp2;Vv2{L8 zxHDSL+UwdC^11T?OHQ(cdUnSLY#mZQfK^5ksa#SrIh)KTF&Q0>B&CFylqzf6e&=y- z(7naF%5NWP3-?;OEzwXY-W*K#v{hy?t`Ns^@pM|mss|&ZS#b)lm%z_FVRBlh(wX?K zh$POX$p*iUJFJ+Y3Q8`|jc|ROkF#^N+!9X3{?2~Se!`a7bL=!b&PG{3>tIc+mid?Y zh53s4kh#d5VWyc$CeDNyAJfd#G1ZJ(_FIYHO#e;)NZ+L2qc6}hy^l`QVcJixq3h|T zG)4VE{Xl(Ay-!`DW~pOTLB47w`I4(@ovM7)Ml_Tbn?S3s=D2irB3eG_AR{cNL2QP) zK+2pzoCL|kDp|OPXUdm8CQe@dHAU{8yGaWAO5S{d`^oU9;Z|u;`cp%#mPS=0C|LS0 z@^{)Oa7uIttzIZ)ML}5 { p[v] = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 }; return p; }, {} as Record>>), + training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0 }; return p; }, {} as Record>>), leveling: { 1: 0 }, abilities: {}, choices: {}, @@ -107,17 +107,7 @@ const defaultCompiledCharacter = (character: Character) => ({ passiveparry: 0, passivedodge: 0, }, - mastery: { - strength: 0, - dexterity: 0, - shield: 0, - armor: 0, - multiattack: 1, - magicpower: 0, - magicspeed: 0, - magicelement: 0, - magicinstinct: 0, - }, + mastery: [], bonus: { abilities: {}, 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.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 @@ -397,21 +388,21 @@ export class CharacterCompiler 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) return; config.features[feature]?.effect.forEach((effect) => this.apply(effect)); } - protected remove(feature?: string) + protected remove(feature?: FeatureID) { if(!feature) return; config.features[feature]?.effect.forEach((effect) => this.undo(effect)); } - protected apply(feature?: FeatureItem | FeatureEquipment) + protected apply(feature?: FeatureItem) { if(!feature) return; @@ -449,7 +440,7 @@ export class CharacterCompiler return; } } - protected undo(feature?: FeatureItem | FeatureEquipment) + protected undo(feature?: FeatureItem) { if(!feature) return; @@ -542,11 +533,7 @@ export class CharacterCompiler if(stop === true) continue; - const path = property.split("/"); - 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); + setProperty(this._result, property, Math.max(sum, this._buffer[property]!.min)); this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min); this._buffer[property]!._dirty = false; @@ -554,6 +541,14 @@ export class CharacterCompiler } } } +function setProperty(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 { private _container: RedrawableHTML; @@ -1253,6 +1248,22 @@ class AspectPicker extends BuilderTab } } +export const masteryTexts: Record = { + "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 Rarity = ItemConfig['rarity']; export const colorByRarity: Record = { @@ -1272,6 +1283,7 @@ export const weaponTypeTexts: Record = { "finesse": 'maniable', "reach": 'longue', "projectile": 'à projectile', + "improvised": "improvisée" } export const armorTypeTexts: Record = { '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") ]), - () => 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 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, @@ -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 > 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, - ]) : undefined, + ]) : undefined, */ 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, @@ -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 divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2 gap-4', [ span('italic text-sm', `Puissance magique: ${enchant.power}`), - button(icon('radix-icons:plus', { width: 16, height: 16 }), () => { - current.item!.enchantments?.push(enchant.id); - // TODO: Object.assign(current.item!.state, current.item!.enchantments?.reduce((p, id) => { config.enchantments[id]?.effect.filter(e => e.category === "value" && e.property.startsWith('item/')); return p; }, {})); + button(icon(() => current.item?.enchantments?.includes(enchant.id) ? 'radix-icons:minus' : 'radix-icons:plus', { width: 16, height: 16 }), () => { + const idx = current.item!.enchantments?.findIndex(e => e === enchant.id) ?? -1; + if(idx === -1) + current.item!.enchantments?.push(enchant.id); + else + current.item!.enchantments?.splice(idx, 1); + + current.item!.state ??= {}; this.character?.saveVariables(); }, 'p-1 !border-solid !border-r'), diff --git a/shared/components.util.ts b/shared/components.util.ts index a1b914c..c3e461c 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -344,7 +344,7 @@ export function combobox>(options: Option[], setti 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 }; let value: T | undefined = undefined; diff --git a/shared/feature.util.ts b/shared/feature.util.ts index ed57383..e822113 100644 --- a/shared/feature.util.ts +++ b/shared/feature.util.ts @@ -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 { MarkdownEditor } from "#shared/editor.util"; 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 { 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 { getID } from "#shared/general.util"; import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util"; @@ -491,7 +491,7 @@ export class HomebrewBuilder } } -type FeatureOption = Partial & { id: string }; +type FeatureOption = Partial & { id: string }; class FeatureEditor { private _list: Record | FeatureOption[]; @@ -576,11 +576,13 @@ class FeatureEditor switch(effect.category) { 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': return flattenFeatureChoices.findLast(e => e.category === 'choice'); + case 'tree': + return flattenFeatureChoices.findLast(e => e.category === 'tree'); 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) @@ -594,6 +596,8 @@ class FeatureEditor return this.editList(buffer as Partial); case 'choice': return this.editChoice(buffer as Partial); + case 'tree': + return this.editTree(buffer as Partial); default: break; } return { top, bottom }; @@ -620,21 +624,20 @@ class FeatureEditor } private editList(buffer: Partial) { - let list: Option[]; - if(buffer.action === 'add') + let list: Option[] = []; + 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 })); - } - else if(buffer.list) - { + break; + 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 })); - } - } - 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 })); + break; } return { @@ -642,7 +645,22 @@ class FeatureEditor buffer.action = value as 'add' | 'remove'; this.edit(); }, 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) + { + 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) @@ -709,7 +727,7 @@ class FeatureEditor ...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 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(); @@ -880,17 +898,8 @@ const featureChoices: Option>[] = [ { 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: 'Maitrise', value: [ - { text: 'Maitrise des armes (for.)', value: { category: 'value', property: 'mastery/strength', operation: 'add', value: 1 } }, - { 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: 'Arbre', value: { category: 'tree', option: 0 } }, + { 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: 'Compétences', value: [ ...ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option>[], { 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): string { 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`; }