Item Improvements added to the homebrew manager.

This commit is contained in:
2026-06-08 16:53:54 +02:00
parent 3bafc14255
commit f9e0473b2a
11 changed files with 204 additions and 99 deletions

View File

@@ -1,10 +1,10 @@
import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureState, FeatureTree, FeatureValue, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character";
import type { Ability, ArmorConfig, AspectConfig, CharacterConfig, CommonItemConfig, CraftingType, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureState, FeatureTree, FeatureValue, ImprovementConfig, ItemConfig, Level, MainStat, MundaneConfig, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponConfig, WeaponType, WondrousConfig } from "~/types/character";
import { div, dom, icon, span, text, type NodeChildren } from "#shared/dom";
import { MarkdownEditor } from "#shared/editor";
import { preview } from "#shared/proses";
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, toggle, type Option } from "#shared/components";
import { button, checkbox, combobox, foldable, input, multiselect, numberpicker, optionmenu, select, tabgroup, table, tagpicker, toggle, type Option } from "#shared/components";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, colorByRarity, DAMAGE_TYPES, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, masteryTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, subnameFactory, weaponTypeTexts } from "#shared/character";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, colorByRarity, craftingText, DAMAGE_TYPES, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, masteryTexts, rarityText, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts, subnameFactory, weaponTypeTexts } from "#shared/character";
import characterConfig from "#shared/character-config.json";
import { getID } from "#shared/general";
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown";
@@ -32,6 +32,7 @@ export class HomebrewBuilder
{ id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() },
{ id: 'actions', title: [ text("Actions") ], content: () => this.actions() },
{ id: 'items', title: [ text("Objets") ], content: () => this.items() },
{ id: 'improvements', title: [ text("Améliorations") ], content: () => this.improvements() },
{ id: 'trees', title: [ text("Arbres") ], content: () => this.trees() },
], { focused: 'training', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } });
@@ -512,6 +513,7 @@ export class HomebrewBuilder
rarity: 'common',
equippable: false,
consummable: false,
craft: { natural: 0, mineral: 0, processed: 0, magical: 0 },
};
switch(category)
{
@@ -530,7 +532,7 @@ export class HomebrewBuilder
config.items[item.id!] = item;
};
const remove = (item: ItemConfig) => {
confirm(`Voulez vous vraiment supprimer l'effet "${item.name}" ?`).then(e => {
confirm(`Voulez vous vraiment supprimer l'objet "${item.name}" ?`).then(e => {
if(e)
{
delete config.texts[item.description];
@@ -568,6 +570,59 @@ export class HomebrewBuilder
}
}) ] ) ];
}
improvements()
{
const defaultImprovement = (): ImprovementConfig => {
return {
id: getID(),
name: '',
description: getID(), // i18nID
craft: {},
rarity: 'common',
cursed: false,
power: 0,
effect: [],
};
};
const add = () => {
const improvement = defaultImprovement();
setText(improvement.description, '');
config.improvements[improvement.id!] = improvement;
};
const remove = (improvement: ImprovementConfig) => {
confirm(`Voulez vous vraiment supprimer l'amélioration "${improvement.name}" ?`).then(e => {
if(e)
{
delete config.texts[improvement.description];
delete config.improvements[improvement.id];
}
});
};
const edit = (improvement: ImprovementConfig) => {
ImprovementPanel.edit(improvement).then(f => {
Object.assign(config.improvements[f.id]!, f);
}).catch((e) => {});
}
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), div('grid grid-cols-2 gap-1', {
list: () => Object.keys(config.improvements),
render: (e, _c) => {
const improvement = config.improvements[e];
if(!improvement) return;
return _c ?? div('border border-light-35 dark:border-dark-35 p-1 gap-2', [ div('flex flex-row justify-between', [
div('flex flex-row items-center gap-4 ps-2', [ span(() => [colorByRarity[improvement.rarity], 'text-lg font-semibold'], () => improvement.name) ]),
div('flex flex-row gap-1', [
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
div('flex flex-row w-12 gap-2 justify-between items-center px-2', [ icon('game-icons:bolt-drop', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('', () => improvement.power) ]),
]),
button(icon('radix-icons:pencil-2'), () => edit(config.improvements[e]!), 'p-1'),
button(icon('radix-icons:trash'), () => remove(config.improvements[e]!), 'p-1'),
])
]), div('px-2 pb-1', [ () => markdown(getText(improvement.description)) ]) ]);
}
}) ] ) ];
}
trees()
{
const add = () => {
@@ -976,8 +1031,13 @@ export class ItemPanel
}, 'p-1'), 'Annuler', 'left'),
]),
foldable([
div('flex flex-row col-span-2 gap-2 items-center justify-between', [ span('flex flex-row gap-2 items-center', 'Fabrication'), div('flex flex-row items-center gap-2 w-1/2', [ select<CraftingType>(Object.entries(craftingText).map(e => ({ text: e[1], value: e[0] as CraftingType })), { defaultValue: _item.craft.ability ?? 'crafter', change: (v) => _item.craft.ability = v, class: { container: '!w-1/2' } }), numberpicker({ defaultValue: _item.craft.difficulty ?? 0, input: (v) => _item.craft.difficulty = v, class: 'w-12' }) ]), div('flex flex-row items-center gap-1', [
numberpicker({ defaultValue: _item.craft.natural, disabled: _item.craft.natural === undefined, input: (v) => _item.craft.natural = v, class: 'w-10' }),
numberpicker({ defaultValue: _item.craft.mineral, disabled: _item.craft.mineral === undefined, input: (v) => _item.craft.mineral = v, class: 'w-10' }),
numberpicker({ defaultValue: _item.craft.processed, disabled: _item.craft.processed === undefined, input: (v) => _item.craft.processed = v, class: 'w-10' }),
numberpicker({ defaultValue: _item.craft.magical, disabled: _item.craft.magical === undefined, input: (v) => _item.craft.magical = v, class: 'w-10' }),
]) ]),
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.weight !== undefined, change: function(value) { _item.weight = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Poids'), ]), numberpicker({ defaultValue: _item.weight, disabled: _item.weight === undefined, input: (v) => _item.weight = v, class: '!w-1/3' }), ]),
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.price !== undefined, change: function(value) { _item.price = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Prix'), ]), numberpicker({ defaultValue: _item.price, disabled: _item.price === undefined, input: (v) => _item.price = v, class: '!w-1/3' }), ]),
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.capacity !== undefined, change: function(value) { _item.capacity = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Capacité magique'), ]), numberpicker({ defaultValue: _item.capacity, disabled: _item.capacity === undefined, input: (v) => _item.capacity = v, class: '!w-1/3' }), ]),
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.powercost !== undefined, change: function(value) { _item.powercost = value ? 0 : undefined; if(this.parentElement?.parentElement?.children[1]) { (this.parentElement.parentElement.children[1] as Element & { disabled: boolean }).disabled = !value; (this.parentElement.parentElement.children[1] as HTMLInputElement).value = value ? '0' : ''; } this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Puissance magique'), ]), numberpicker({ defaultValue: _item.powercost, disabled: _item.powercost === undefined, input: (v) => _item.powercost = v, class: '!w-1/3' }), ]),
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.equippable, change: function(value) { _item.equippable = value; this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Equipable'), ]), div('flex flex-row gap-2 items-center mx-4', [ checkbox({ defaultValue: _item.consummable, change: function(value) { _item.consummable = value; this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Consommable'), ]) ]),
@@ -1020,6 +1080,61 @@ export class ItemPanel
});
}
}
export class ImprovementPanel
{
static descriptionEditor: MarkdownEditor = new MarkdownEditor();
static render(improvement: ImprovementConfig, success: (improvement: ImprovementConfig) => void, failure: (improvement: ImprovementConfig) => void)
{
const _improvement = JSON.parse(JSON.stringify(improvement)) as ImprovementConfig;
ImprovementPanel.descriptionEditor.content = getText(_improvement.description);
ImprovementPanel.descriptionEditor.onChange = (value) => setText(_improvement.description, value);
const effectContainer = div('grid grid-cols-2 gap-4 px-2 flex-1', _improvement.effect?.map(e => new FeatureEditor(_improvement.effect!, e.id, false).container));
return dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [
div('flex flex-row justify-between items-center', [
tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => {
success!(_improvement);
ImprovementPanel.descriptionEditor.onChange = undefined;
}, 'p-1'), 'Valider', 'left'),
dom('label', { class: 'flex justify-center items-center my-2' }, [
dom('span', { class: 'pb-1 md:p-0', text: "Nom" }),
input('text', { defaultValue: _improvement.name, input: (v) => { _improvement.name = v }, class: 'w-96' })
]),
tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => {
failure!(improvement);
ImprovementPanel.descriptionEditor.onChange = undefined;
}, 'p-1'), 'Annuler', 'left'),
]),
foldable([
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Puissance magique'), ]), numberpicker({ defaultValue: _improvement.power, disabled: _improvement.power === undefined, input: (v) => _improvement.power = v, class: '!w-1/3' }), ]),
div('flex flex-row gap-2 items-center justify-between', [ span('flex flex-row gap-2 items-center', 'Fabrication'), div('flex flex-row items-center gap-2 !w-2/3', [ select<CraftingType>(Object.entries(craftingText).map(e => ({ text: e[1], value: e[0] as CraftingType })), { defaultValue: _improvement.craft.ability ?? 'crafter', change: (v) => _improvement.craft.ability = v, class: { container: '!w-1/2' } }), numberpicker({ defaultValue: _improvement.craft.difficulty ?? 0, input: (v) => _improvement.craft.difficulty = v, class: 'w-12' }) ]) ]),
], [ span('text-lg font-bold', "Propriétés"), div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Rareté'), ]), select(Object.keys(rarityText).map(e => ({ text: rarityText[e as Rarity], value: e as Rarity })), { defaultValue: _improvement.rarity, change: (v) => _improvement.rarity = v, class: { container: '!w-1/2' } }), ]) ], { class: { content: 'group-data-[active]:grid grid-cols-2 my-2 gap-4', title: 'grid grid-cols-2 gap-4 mx-2', container: 'pb-2 border-b border-light-35 dark:border-dark-35' }, open: true } ),
foldable([ div('p-1 border border-light-40 dark:border-dark-40 w-full bg-light-25 dark:bg-dark-25 min-h-48 max-h-[32rem]', [ ImprovementPanel.descriptionEditor.dom ])], [ span('text-lg font-bold px-2', "Description des effets") ], { class: { container: 'gap-4 pb-2 border-b border-light-35 dark:border-dark-35' }, open: true, }),
div('flex flex-row gap-4 items-center pb-2 border-b border-light-35 dark:border-dark-35', [ dom('h3', { class: 'text-lg font-bold', text: 'Restrictions' }), tagpicker([], { defaultValue: Object.keys(improvement.restrictions ?? {}), class: { container: 'data-[focused]:shadow-raw transition-[box-shadow] data-[focused]:shadow-light-40 dark:data-[focused]:shadow-dark-40' } }) ]),
foldable([ effectContainer ], [ dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => {
const f = { id: getID(), };
_improvement.effect ??= [];
_improvement.effect.push(f as any as FeatureValue | FeatureEquipment | FeatureList);
effectContainer.appendChild(new FeatureEditor(_improvement.effect, f.id, true).container);
}, 'p-1 hidden group-data-[active]:block'), 'Ajouter', 'left'),
], { class: { container: 'flex flex-col gap-2 w-full', title: 'flex flex-row justify-between px-2' } })
]);
}
static edit(improvement: ImprovementConfig): Promise<ImprovementConfig>
{
let container: HTMLElement, close: Function;
return new Promise<ImprovementConfig>((success, failure) => {
container = ImprovementPanel.render(improvement, success, failure);
close = fullblocker([container], {
priority: true, closeWhenOutside: false,
}).close;
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
}).finally(() => {
setTimeout(close, 150);
container.setAttribute('data-state', 'inactive');
});
}
}
const featureChoices: Option<Partial<FeatureOption>>[] = [
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 1 }, },