799 lines
66 KiB
TypeScript
799 lines
66 KiB
TypeScript
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
|
|
import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util";
|
|
import { MarkdownEditor } from "#shared/editor.util";
|
|
import { fakeA } from "#shared/proses";
|
|
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
|
|
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
|
|
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
|
import characterConfig from "#shared/character-config.json";
|
|
import { getID } from "#shared/general.util";
|
|
import renderMarkdown, { renderText } from "#shared/markdown.util";
|
|
import { Tree } from "#shared/tree";
|
|
import markdownUtil from "#shared/markdown.util";
|
|
import { getText } from "#shared/i18n";
|
|
|
|
const config = characterConfig as CharacterConfig;
|
|
export class HomebrewBuilder
|
|
{
|
|
private _container: HTMLDivElement;
|
|
private _content?: HTMLDivElement;
|
|
private _tabsHeader: HTMLDivElement[] = [];
|
|
private _tabsContent: BuilderTab[] = [];
|
|
|
|
private _config: CharacterConfig;
|
|
private _editor: FeatureEditor;
|
|
|
|
constructor(container: HTMLDivElement)
|
|
{
|
|
this._config = config as CharacterConfig;
|
|
this._editor = new FeatureEditor();
|
|
this._container = container;
|
|
|
|
this._tabsHeader = [
|
|
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(0) } }, [text("Peuples")]),
|
|
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(1) } }, [text("Entrainement")]),
|
|
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(2) } }, [text("Aspect")]),
|
|
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(3) } }, [text("Sorts")]),
|
|
//dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(4) } }, [text("Listes")]),
|
|
];
|
|
this._tabsContent = [
|
|
new PeopleEditor(this, this._config),
|
|
new TrainingEditor(this, this._config),
|
|
new AspectEditor(this, this._config),
|
|
new SpellEditor(this, this._config),
|
|
];
|
|
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
|
|
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
|
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [ div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._tabsHeader), tooltip(button(icon('radix-icons:clipboard-copy', { width: 16, height: 16 }), () => this.save(), 'p-2'), 'Copier le JSON', 'left') ]),
|
|
this._content,
|
|
]));
|
|
|
|
this.display(1);
|
|
}
|
|
display(tab: number)
|
|
{
|
|
if(tab < 0 || tab >= this._tabsHeader.length)
|
|
return;
|
|
|
|
this._tabsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
|
|
this._tabsHeader[tab]!.setAttribute('data-state', 'active');
|
|
|
|
this._content?.replaceChildren(...this._tabsContent[tab]!.dom);
|
|
}
|
|
edit(feature: Feature): Promise<Feature>
|
|
{
|
|
const promise: Promise<Feature> = this._editor.edit(feature).then(f => {
|
|
this._config.features[feature.id] = f;
|
|
return f;
|
|
}).catch(() => feature).finally(() => {
|
|
setTimeout(popup.close, 150);
|
|
this._editor.container.setAttribute('data-state', 'inactive');
|
|
});
|
|
const popup = fullblocker([this._editor.container], {
|
|
priority: true, closeWhenOutside: false,
|
|
});
|
|
setTimeout(() => this._editor.container.setAttribute('data-state', 'active'), 1);
|
|
return promise;
|
|
}
|
|
private save()
|
|
{
|
|
navigator.clipboard.writeText(JSON.stringify(this._config));
|
|
}
|
|
}
|
|
abstract class BuilderTab {
|
|
protected _builder: HomebrewBuilder;
|
|
protected _config: CharacterConfig;
|
|
protected _content!: Array<Node | string>;
|
|
|
|
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
{
|
|
this._builder = builder;
|
|
this._config = config;
|
|
}
|
|
get dom()
|
|
{
|
|
return this._content;
|
|
}
|
|
};
|
|
class PeopleEditor extends BuilderTab
|
|
{
|
|
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
{
|
|
super(builder, config);
|
|
|
|
const add = () => {
|
|
const people: RaceConfig = {
|
|
id: getID(),
|
|
name: '',
|
|
description: '',
|
|
options: LEVELS.map(e => {
|
|
const feature: Feature = {
|
|
id: getID(),
|
|
description: '',
|
|
effect: [],
|
|
}
|
|
config.features[feature.id] = feature;
|
|
return [e, [feature.id]] as [Level, string[]];
|
|
}).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<Level, string[]>)
|
|
};
|
|
config.peoples[people.id] = people;
|
|
(this._content[0] as HTMLDivElement).appendChild(peopleRender(people));
|
|
}
|
|
const remove = (people: RaceConfig) => {
|
|
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
|
|
if(e)
|
|
{
|
|
Object.values(people.options).forEach(e => e.forEach(id => delete config.features[id]));
|
|
delete config.peoples[people.id];
|
|
|
|
|
|
}
|
|
})
|
|
}
|
|
const render = (people: string, level: Level, feature: string) => {
|
|
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
|
|
this._builder.edit(config.features[feature]!).then(e => {
|
|
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
|
|
});
|
|
}, contextmenu: (e) => {
|
|
e.preventDefault();
|
|
const context = contextmenu(e.clientX, e.clientY, [
|
|
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
|
|
context.close();
|
|
const _feature: Feature = { id: getID(), description: '', effect: [] };
|
|
config.features[_feature.id] = _feature;
|
|
config.peoples[people]!.options[level]!.push(_feature.id);
|
|
element.parentElement?.appendChild(render(people, level, _feature.id));
|
|
} } }, [ text('Nouveau') ]),
|
|
config.peoples[people]!.options[level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
|
|
context.close();
|
|
confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) {
|
|
config.peoples[people]!.options[level] = config.peoples[people]!.options[level].filter(e => e !== feature);
|
|
delete config.features[feature];
|
|
element.remove();
|
|
}
|
|
}) } } }, [ text('Supprimer') ]) : undefined,
|
|
], { placement: "right-start", priority: false });
|
|
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
|
|
return element;
|
|
}
|
|
const peopleRender = (people: RaceConfig) => {
|
|
return foldable(() => Object.entries(people.options).flatMap(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) => render(people.id, parseInt(level[0], 10) as Level, option))),
|
|
]), [ input('text', { defaultValue: people.name, input: (value) => people.name = value, class: 'w-32' }), input('text', { defaultValue: people.description, input: (value) => people.description = value, class: 'w-full' }) ], { class: { container: 'gap-2 max-h-full', title: 'flex flex-row', content: 'flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8' }, open: false })
|
|
}
|
|
const container = div('flex flex-col gap-2', Object.values(config.peoples).map(peopleRender));
|
|
this._content = [ div('flex flex-col py-2 gap-2', [ div('w-full flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), container ]) ];
|
|
}
|
|
}
|
|
class TrainingEditor extends BuilderTab
|
|
{
|
|
private _options: Record<MainStat, HTMLDivElement[][]>;
|
|
|
|
private _tab: number = 0;
|
|
private _statIndicator: HTMLSpanElement;
|
|
private _statContainer: HTMLDivElement;
|
|
|
|
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
{
|
|
super(builder, config);
|
|
const render = (stat: MainStat, level: TrainingLevel, feature: string) => {
|
|
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
|
|
this._builder.edit(config.features[feature]!).then(e => {
|
|
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
|
|
});
|
|
}, contextmenu: (e) => {
|
|
e.preventDefault();
|
|
const context = contextmenu(e.clientX, e.clientY, [
|
|
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
|
|
context.close();
|
|
const _feature: Feature = { id: getID(), description: '', effect: [] };
|
|
config.features[_feature.id] = _feature;
|
|
config.training[stat][level].push(_feature.id);
|
|
element.parentElement?.appendChild(render(stat, level, _feature.id));
|
|
} } }, [ text('Nouveau') ]),
|
|
config.training[stat][level].length > 1 ? dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
|
|
context.close();
|
|
confirm('Voulez-vous vraiment supprimer cet element ?').then(e => { if(e) {
|
|
config.training[stat][level as any as TrainingLevel] = config.training[stat][level as any as TrainingLevel].filter(e => e !== feature);
|
|
delete config.features[feature];
|
|
element.remove();
|
|
}
|
|
}) } } }, [ text('Supprimer') ]) : undefined,
|
|
], { placement: "right-start", priority: false });
|
|
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
|
|
return element;
|
|
};
|
|
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) => render(stat, parseInt(level[0], 10) as TrainingLevel, option))),
|
|
])
|
|
}
|
|
|
|
this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, HTMLDivElement[][]>);
|
|
|
|
this._statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' });
|
|
this._statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(this._options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e]))));
|
|
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20", [
|
|
div('flex flex-shrink gap-3 items-center relative w-48 ms-12', [
|
|
...MAIN_STATS.map((e, i) => dom('span', { listeners: { click: () => this.switchTab(i) }, class: 'block w-2.5 h-2.5 m-px outline outline-1 outline-transparent hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer' })),
|
|
this._statIndicator,
|
|
]),
|
|
]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])];
|
|
|
|
this.switchTab(0);
|
|
}
|
|
switchTab(tab: number)
|
|
{
|
|
this._tab = tab;
|
|
|
|
this._statIndicator.setAttribute('data-text', mainStatTexts[MAIN_STATS[tab] as MainStat]);
|
|
this._statIndicator.style.left = `${tab * 1.5}em`;
|
|
|
|
this._statContainer.style.left = `-${tab * 100}%`;
|
|
}
|
|
}
|
|
class AspectEditor extends BuilderTab
|
|
{
|
|
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
{
|
|
super(builder, config);
|
|
|
|
const render = (aspect: AspectConfig) => {
|
|
return {
|
|
name: input('text', { input: (value) => aspect.name = value, defaultValue: aspect.name, class: '!m-0 w-full' }),
|
|
description: input('text', { input: (value) => aspect.description = value, defaultValue: aspect.description, class: '!m-0 w-full' }),
|
|
stat: select(MAIN_STATS.map(f => ({ text: mainStatTexts[f], value: f })), { change: (value) => aspect.stat = value, defaultValue: aspect.stat, class: { container: '!m-0 w-full' } }),
|
|
alignment: select(ALIGNMENTS.map(f => ({ text: alignmentTexts[f], value: f })), { change: (value) => aspect.alignment = value, defaultValue: aspect.alignment, class: { container: '!m-0 w-full' } }),
|
|
magic: toggle({ defaultValue: aspect.magic, change: (value) => aspect.magic = value, class: { container: '' } }),
|
|
difficulty: numberpicker({ min: 6, max: 13, input: (value) => aspect.difficulty = value, defaultValue: aspect.difficulty, class: '!m-0 w-full' }),
|
|
physic: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.physic.min, input: (value) => aspect.physic.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.physic.max, input: (value) => aspect.physic.max = value, class: '!m-0' }) ]),
|
|
mental: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.mental.min, input: (value) => aspect.mental.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.mental.max, input: (value) => aspect.mental.max = value, class: '!m-0' }) ]),
|
|
personality: div('flex flex-row justify-center', [ numberpicker({ defaultValue: aspect.personality.min, input: (value) => aspect.personality.min = value, class: '!m-0' }), numberpicker({ defaultValue: aspect.personality.max, input: (value) => aspect.personality.max = value, class: '!m-0' }) ]),
|
|
action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:file-text'), () => {}, 'p-1'), button(icon('radix-icons:trash'), () => remove(aspect), 'p-1') ])
|
|
};
|
|
}
|
|
const add = () => {
|
|
config.aspects.push({
|
|
name: '',
|
|
description: '',
|
|
stat: 'strength',
|
|
alignment: 'loyal_good',
|
|
magic: false,
|
|
difficulty: 6,
|
|
physic: { min: 0, max: 30 },
|
|
mental: { min: 0, max: 20 },
|
|
personality: { min: 0, max: 20 },
|
|
options: []
|
|
});
|
|
|
|
const element = redraw();
|
|
content.parentElement?.replaceChild(element, content);
|
|
content = element;
|
|
};
|
|
const remove = (aspect: AspectConfig) => {
|
|
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
|
|
if(e)
|
|
{
|
|
config.aspects = config.aspects.filter(e => e !== aspect);
|
|
|
|
const element = redraw();
|
|
content.parentElement?.replaceChild(element, content);
|
|
content = element;
|
|
}
|
|
})
|
|
}
|
|
const redraw = () => table(config.aspects.map(render), { name: 'Nom', description: 'Description', stat: 'Buff de stat', alignment: 'Alignement', magic: 'Magie', difficulty: 'Difficulté', physic: 'Physique', mental: 'Mental', personality: 'Caractère', action: 'Actions' }, { class: { table: 'flex-1' } });
|
|
let content = redraw();
|
|
this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
|
|
}
|
|
}
|
|
class SpellEditor extends BuilderTab
|
|
{
|
|
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
{
|
|
super(builder, config);
|
|
|
|
const render = (spell: SpellConfig) => {
|
|
return foldable([
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]),
|
|
], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-64' }), input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash', { noobserver: true }), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
|
|
}
|
|
const add = () => {
|
|
config.spells.push({
|
|
id: getID(),
|
|
name: '',
|
|
rank: 1,
|
|
type: 'precision',
|
|
cost: 1,
|
|
speed: 'action',
|
|
elements: [],
|
|
effect: '',
|
|
concentration: false,
|
|
tags: [],
|
|
});
|
|
|
|
const element = redraw();
|
|
content.parentElement?.replaceChild(element, content);
|
|
content = element;
|
|
};
|
|
const remove = (spell: SpellConfig) => {
|
|
confirm('Voulez vous vraiment supprimer ce sort ?').then(e => {
|
|
if(e)
|
|
{
|
|
config.spells = config.spells.filter(e => e !== spell);
|
|
|
|
const element = redraw();
|
|
content.parentElement?.replaceChild(element, content);
|
|
content = element;
|
|
}
|
|
});
|
|
}
|
|
const redraw = () => div('flex flex-col divide-y', config.spells.map(render));
|
|
//, { class: { table: 'flex-1' } });
|
|
let content = redraw();
|
|
this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
|
|
}
|
|
}
|
|
|
|
export class FeatureEditor
|
|
{
|
|
private _container: HTMLDivElement;
|
|
|
|
private _success?: Function;
|
|
private _failure?: Function;
|
|
private _feature?: Feature;
|
|
|
|
private _idInput: HTMLInputElement;
|
|
private _table: HTMLDivElement;
|
|
|
|
constructor()
|
|
{
|
|
this._idInput = dom("input", { attributes: { 'disabled': true }, class: `mx-4 text-light-70 dark:text-dark-70 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-25 dark:bg-dark-25 border-light-30 dark:border-dark-30` });
|
|
this._table = div('grid grid-cols-2 gap-4 px-2');
|
|
this._container = 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 }), () => {
|
|
this._success!(this._feature);
|
|
MarkdownEditor.singleton.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: "ID" }),
|
|
this._idInput
|
|
]),
|
|
tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => {
|
|
this._failure!(this._feature);
|
|
MarkdownEditor.singleton.onChange = undefined;
|
|
}, 'p-1'), 'Annuler', 'left'),
|
|
]),
|
|
dom('span', { class: 'flex flex-col justify-start items-start my-2 gap-4' }, [
|
|
div('flex w-full items-center justify-between', [
|
|
dom('span', { class: 'pb-1 md:p-0', text: "Description" }),
|
|
tooltip(button(icon('radix-icons:clipboard', { width: 20, height: 20 }), () => {
|
|
MarkdownEditor.singleton.content = this._feature?.effect.map(e => textFromEffect(e)).join('\n') ?? this._feature?.description ?? MarkdownEditor.singleton.content;
|
|
if(this._feature?.description) this._feature.description = MarkdownEditor.singleton.content;
|
|
}, 'p-1'), 'Description automatique', 'left'),
|
|
]),
|
|
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]', [ MarkdownEditor.singleton.dom ]),
|
|
]),
|
|
div('flex flex-col gap-2 w-full', [
|
|
div('flex flex-row justify-between', [
|
|
dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
|
|
tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => {
|
|
this._table.appendChild(this._edit({ id: getID() }));
|
|
}, 'p-1'), 'Ajouter', 'left'),
|
|
]),
|
|
this._table,
|
|
])
|
|
]);
|
|
}
|
|
edit(feature: Feature): Promise<Feature>
|
|
{
|
|
return new Promise((success, failure) => {
|
|
this._success = success;
|
|
this._failure = failure;
|
|
|
|
this._feature = JSON.parse(JSON.stringify(feature)) as Feature;
|
|
|
|
this._table.replaceChildren(...this._feature.effect.map(this._renderEffect.bind(this)));
|
|
this._idInput.value = this._feature.id;
|
|
MarkdownEditor.singleton.onChange = (e) => this._feature!.description = e;
|
|
MarkdownEditor.singleton.content = this._feature.description;
|
|
});
|
|
}
|
|
private _renderEffect(effect: Partial<FeatureItem>): HTMLDivElement
|
|
{
|
|
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-center', [
|
|
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
|
|
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
|
|
content.replaceWith(this._edit(effect));
|
|
}, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifieur", "bottom"), tooltip(button(icon('radix-icons:trash'), () => {
|
|
this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id);
|
|
content.remove();
|
|
}, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Supprimer", "bottom") ])
|
|
]) ]);
|
|
return content;
|
|
}
|
|
private _edit(effect: Partial<FeatureItem>): HTMLDivElement
|
|
{
|
|
const match = (effect: FeatureItem): Partial<FeatureItem> | undefined => {
|
|
switch(effect.category)
|
|
{
|
|
case 'value':
|
|
return flattenFeatureChoices.findLast(e => e.category === 'value' && e.property === effect.property);
|
|
case 'choice':
|
|
return flattenFeatureChoices.findLast(e => e.category === 'choice');
|
|
case 'list':
|
|
return flattenFeatureChoices.findLast(e => e.category === 'list' && e.list === effect.list);
|
|
}
|
|
};
|
|
const approve = () => {
|
|
const idx = this._feature!.effect.findIndex(e => e.id === _buffer.id);
|
|
|
|
if(idx === -1)
|
|
this._feature!.effect.push(_buffer);
|
|
else
|
|
this._feature!.effect[idx] = _buffer;
|
|
|
|
this._table.replaceChild(this._renderEffect(_buffer), content);
|
|
}, reject = () => {
|
|
const idx = this._feature!.effect.findIndex(e => e.id === _buffer.id);
|
|
|
|
if(idx === -1)
|
|
content.remove();
|
|
else
|
|
this._table.replaceChild(this._renderEffect(effect), content);
|
|
}
|
|
let _buffer = JSON.parse(JSON.stringify(effect)) as FeatureItem;
|
|
|
|
const drawByCategory = (buffer: Partial<FeatureItem>) => {
|
|
let top: NodeChildren = [], bottom: NodeChildren = [];
|
|
switch(buffer.category)
|
|
{
|
|
case 'value':
|
|
const valueVariable = () => typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } });
|
|
const summaryText = text(textFromEffect(buffer));
|
|
let valueSelection = valueVariable();
|
|
top = [
|
|
select([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].includes(buffer.property ?? '') || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }),
|
|
valueSelection,
|
|
tooltip(button(icon('radix-icons:update'), () => {
|
|
(buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
|
|
const newValueSelection = valueVariable();
|
|
valueSelection.replaceWith(newValueSelection);
|
|
valueSelection = newValueSelection;
|
|
summaryText.textContent = textFromEffect(buffer);
|
|
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Changer d\'editeur', 'bottom'),
|
|
];
|
|
bottom = [ div('px-2 py-1 flex items-center flex-1', [summaryText]) ];
|
|
break;
|
|
case 'list':
|
|
if(buffer.action === 'add')
|
|
{
|
|
if(buffer.list === 'spells')
|
|
{
|
|
bottom = [ combobox(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(renderText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ];
|
|
}
|
|
else
|
|
{
|
|
const editor = new MarkdownEditor();
|
|
editor.content = getText(buffer.item);
|
|
editor.onChange = (item) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = item;
|
|
|
|
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1 flex items-center', [ editor.dom ]) ];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map(e => ({ text: buffer.list !== 'spells' ? renderText(getText((e as Extract<FeatureItem, { category: 'list' }>).item)) : config.spells.find(f => f.id === (e as Extract<FeatureItem, { category: 'list' }>).item)?.name ?? '', value: (e as Extract<FeatureItem, { category: 'list' }>).item })), { 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' }) ];
|
|
}
|
|
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => {
|
|
(buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove';
|
|
const element = redraw();
|
|
content.replaceWith(element);
|
|
content = element;
|
|
}, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ];
|
|
break;
|
|
case 'choice':
|
|
const add = () => {
|
|
const option: Extract<FeatureItem, { category: 'choice' }>["options"][number] = { id: getID(), category: 'value', text: '', operation: 'add', property: '', value: 0 };
|
|
(buffer as Extract<FeatureItem, { category: 'choice' }>).options.push(option);
|
|
list.appendChild(render(option, true));
|
|
};
|
|
const render = (option: FeatureEffect & { text: string }, state: boolean): HTMLElement => {
|
|
const { top: _top, bottom: _bottom } = drawByCategory(option);
|
|
const combo = combobox([...featureChoices].filter(e => (e?.value as FeatureItem)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureItem)?.category !== 'choice') : e.value; return e; }), { defaultValue: match(option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => {
|
|
option = { id: option.id, ...e } as FeatureEffect & { text: string };
|
|
const element = render(option, true);
|
|
_content.replaceWith(element);
|
|
_content = element;
|
|
} });
|
|
let _content: HTMLElement = foldable(_bottom, [ div('flex flex-1 justify-between', [ div('flex flex-1 flex-row',[ combo, ..._top, input('text', { defaultValue: option.text, input: (value) => option.text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1', placeholder: 'Description' }) ]), tooltip(button(icon('radix-icons:trash'), () => {
|
|
_content.remove();
|
|
(buffer as Extract<FeatureItem, { category: 'choice' }>).options = (buffer as Extract<FeatureItem, { category: 'choice' }>).options.filter(e => e.id !== option.id);
|
|
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' }, open: state });
|
|
return _content;
|
|
}
|
|
const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => render(e, false)) ?? []);
|
|
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract<FeatureItem, { category: 'choice' }>).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => add(), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Ajouter une option', 'bottom') ];
|
|
bottom = [ list ];
|
|
break;
|
|
default: break;
|
|
}
|
|
return { top, bottom };
|
|
}
|
|
const redraw = () => {
|
|
const { top, bottom } = drawByCategory(_buffer);
|
|
return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [
|
|
div('flex flex-row flex-1', [
|
|
combobox(featureChoices, { defaultValue: match(_buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => {
|
|
_buffer = { id: _buffer.id, ...e } as FeatureItem;
|
|
const element = redraw();
|
|
content.replaceWith(element);
|
|
content = element;
|
|
} }),
|
|
...top,
|
|
]),
|
|
div('flex', [ tooltip(button(icon('radix-icons:check'), approve, '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'), reject, '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) ]);
|
|
}
|
|
|
|
let content = redraw();
|
|
return content;
|
|
}
|
|
get container()
|
|
{
|
|
return this._container;
|
|
}
|
|
}
|
|
|
|
const featureChoices: Option<Partial<FeatureItem>>[] = [
|
|
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 1 }, },
|
|
{ text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 1 }, },
|
|
{ text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 1 }, },
|
|
{ text: 'Nombre d\'œuvres maitrisés', value: { category: 'value', property: 'artslots', operation: 'add', value: 1 }, },
|
|
{ text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 1 }, },
|
|
{ text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 1 }, },
|
|
{ text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 1 }, },
|
|
{ text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 1 }, },
|
|
{ text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 1 }, },
|
|
{ text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, },
|
|
{ text: 'Point d\'action', value: { category: 'value', property: 'action', operation: 'set', value: 1 }, },
|
|
{ text: 'Point de réaction', value: { category: 'value', property: 'reaction', operation: 'set', value: 1 }, },
|
|
{ text: 'Puissance magique', value: { category: 'value', property: 'itempower', operation: 'add', value: 1 }, },
|
|
{ text: 'Spécialisation', value: { category: 'value', property: 'spec', operation: 'add', value: 1 }, },
|
|
{ text: 'Défense', value: [
|
|
{ text: 'Défense max', value: { category: 'value', property: 'defense/hardcap', operation: 'add', value: 1 } },
|
|
{ text: 'Défense fixe', value: { category: 'value', property: 'defense/static', operation: 'add', value: 1 } },
|
|
{ text: 'Parade active', value: { category: 'value', property: 'defense/activeparry', operation: 'add', value: 1 } },
|
|
{ text: 'Parade passive', value: { category: 'value', property: 'defense/passiveparry', 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: '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: '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>>[],
|
|
{ text: 'Max de compétence', value: ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
|
|
] },
|
|
{ text: 'Modifieur', value: [
|
|
{ text: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } },
|
|
{ text: 'Modifieur de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } },
|
|
{ text: 'Modifieur de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } },
|
|
{ text: 'Modifieur d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } },
|
|
{ text: 'Modifieur de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } },
|
|
{ text: 'Modifieur de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } },
|
|
{ text: 'Modifieur de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } },
|
|
{ text: 'Modifieur au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [
|
|
{ text: 'Modifieur de force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 },
|
|
{ text: 'Modifieur de dextérité', category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 },
|
|
{ text: 'Modifieur de constitution', category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 },
|
|
{ text: 'Modifieur d\'intelligence', category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 },
|
|
{ text: 'Modifieur de curiosité', category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 },
|
|
{ text: 'Modifieur de charisme', category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 },
|
|
{ text: 'Modifieur de psyché', egory: 'value', property: 'modifier/psyche', operation: 'add', value: 1 }
|
|
]} as Partial<FeatureItem>}
|
|
] },
|
|
{ text: 'Jet de résistance', value: [
|
|
{ text: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } },
|
|
{ text: 'Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } },
|
|
{ text: 'Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 } },
|
|
{ text: 'Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 } },
|
|
{ text: 'Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 } },
|
|
{ text: 'Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } },
|
|
{ text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } },
|
|
{ text: 'Résistance au choix', value: { category: 'choice', text: '+1 au jet de résistance de ', options: [
|
|
{ text: 'Force', category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 },
|
|
{ text: 'Dextérité', category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 },
|
|
{ text: 'Constitution', category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 },
|
|
{ text: 'Intelligence', category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 },
|
|
{ text: 'Curiosité', category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 },
|
|
{ text: 'Charisme', category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 },
|
|
{ text: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }
|
|
]} as Partial<FeatureItem>}
|
|
] },
|
|
{ text: 'Bonus', value: RESISTANCES.map(e => ({ text: resistanceTexts[e as Resistance], value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) },
|
|
{ text: 'Rang', value: [
|
|
{ text: 'Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } },
|
|
{ text: 'Sorts de savoir', value: { category: 'value', property: 'spellranks/knowledge', operation: 'add', value: 1 } },
|
|
{ text: 'Sorts d\'instinct', value: { category: 'value', property: 'spellranks/instinct', operation: 'add', value: 1 } },
|
|
{ text: 'Œuvres', value: { category: 'value', property: 'spellranks/arts', operation: 'add', value: 1 } },
|
|
] },
|
|
{ text: 'Fatigue supportable', value: { category: 'value', property: 'exhaust', operation: 'add', value: 1 } },
|
|
{ text: 'Action', value: { category: 'list', list: 'action', action: 'add' }, },
|
|
{ text: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, },
|
|
{ text: 'Action libre', value: { category: 'list', list: 'freeaction', action: 'add' }, },
|
|
{ text: 'Passif', value: { category: 'list', list: 'passive', action: 'add' }, },
|
|
{ text: 'Choix', value: { category: 'choice', text: '', options: [] }, },
|
|
];
|
|
const flattenFeatureChoices = Tree.accumulate(featureChoices, 'value', (item) => Array.isArray(item.value) ? undefined : item.value).filter(e => !!e) as Partial<FeatureItem>[];
|
|
function textFromEffect(effect: Partial<FeatureItem>): string
|
|
{
|
|
if(effect.category === 'value')
|
|
{
|
|
if(effect.property === undefined)
|
|
return '';
|
|
|
|
switch(effect.property)
|
|
{
|
|
case 'health':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' PV max.' } }) : textFromValue(effect.value, { prefix: { truely: 'PV max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (PV = interdit).' });
|
|
case 'mana':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' mana max.' } }) : textFromValue(effect.value, { prefix: { truely: 'Mana max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Mana = interdit).' });
|
|
case 'spellslots':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' sort(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Sorts maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Sorts = interdit).' });
|
|
case 'artslots':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' œuvre(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Œuvres maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Œuvres = interdit).' });
|
|
case 'speed':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' case(s) de course.' }, falsely: '+0 cases de course' }) : textFromValue(effect.value, { prefix: { truely: 'Vitesse de course de ' }, suffix: { truely: ' case(s).' }, falsely: 'Déplacement impossible.' });
|
|
case 'capacity':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' unité(s) d\'quipement.' } }) : textFromValue(effect.value, { prefix: { truely: 'Capacité d\'equipement fixé à ' }, suffix: { truely: ' unité(s).' }, falsely: 'Impossible de posséder du materiel.' });
|
|
case 'initiative':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' à l\'itiniative.' } }) : textFromValue(effect.value, { prefix: { truely: 'Initiative fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Initiative = interdit).' });
|
|
case 'training':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) d\'entrainement.' } }) : `Opération interdite (Entrainement fixe).`;
|
|
case 'ability':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) de compétence.' } }) : `Opération interdite (Compétences fixe).`;
|
|
case 'spec':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' spécialisation(s).' } }) : `Opération interdite (Spécialisation fixe).`;
|
|
case 'itempower':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' puissance magique supportable.' } }) : `Opération interdite (Puissance magique fixe).`;
|
|
case 'action':
|
|
return effect.operation === 'add' ? `Opération interdite (Point d'action bonus).` : textFromValue(effect.value, { suffix: { truely: ' point(s) d\'action par tour.' }, falsely: 'Opération interdite (Action = interdit).' });
|
|
case 'reaction':
|
|
return effect.operation === 'add' ? `Opération interdite (Point de réaction bonus).` : textFromValue(effect.value, { suffix: { truely: ' point(s) de réaction par tour.' }, falsely: 'Opération interdite (Réaction = interdit).' });
|
|
case 'exhaust':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Vous êtes capable de supporter ', positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) de fatigue avant de subir les effets de la fatigue.' } }) : `Opération interdite (Fatigue fixe).`;
|
|
default: break;
|
|
}
|
|
|
|
const splited = effect.property.split('/');
|
|
switch(splited[0])
|
|
{
|
|
case 'spellranks':
|
|
switch(splited[1])
|
|
{
|
|
case 'precision':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang de sort de précision.' } }) : `Opération interdite (Rang de sorts de précision fixe).`;
|
|
case 'knowledge':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang de sort de savoir.' } }) : `Opération interdite (Rang de sorts de savoir fixe).`;
|
|
case 'instinct':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang de sort d\'instinct.' } }) : `Opération interdite (Rang de sorts d\'instinct fixe).`;
|
|
case 'arts':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' rang d\'œuvres.' } }) : `Opération interdite (Rang d\'œuvres fixe).`;
|
|
default: return 'Type de sort inconnu.';
|
|
}
|
|
case 'defense':
|
|
switch(splited[1])
|
|
{
|
|
case 'hardcap':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Défense max ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Défense max fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Hardcap = interdit).' });
|
|
case 'static':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Base de défense ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Base de défense fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Static = interdit).' });
|
|
case 'activeparry':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active parry = interdit).' });
|
|
case 'activedodge':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active dodge = interdit).' });
|
|
case 'passiveparry':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive parry = interdit).' });
|
|
case 'passivedodge':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive dodge = interdit).' });
|
|
default: return 'Défense inconnue.';
|
|
}
|
|
case 'mastery':
|
|
switch(splited[1])
|
|
{
|
|
case 'strength':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise for = interdit).' });
|
|
case 'dexterity':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise dex = interdit).' });
|
|
case 'shield':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise boucliers = interdit).' });
|
|
case 'armor':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise armure = interdit).' });
|
|
case 'multiattack':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Attaque multiple ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Attaque multiple fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Attaque multiple = interdit).' });
|
|
case 'magicpower':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise puissance = interdit).' });
|
|
case 'magicspeed':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise rapidité = interdit).' });
|
|
case 'magicelement':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise elements = interdit).' });
|
|
case 'magicinstinct':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise instinct = interdit).' });
|
|
default: return 'Maitrise inconnue.';
|
|
}
|
|
case 'bonus':
|
|
switch(splited[1])
|
|
{
|
|
case 'resistance':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ` aux jets de résistance de ${resistanceTexts[splited[2] as Resistance]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Jets de résistance de ${resistanceTexts[splited[2] as Resistance]} = ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Résistance ${resistanceTexts[splited[2] as Resistance]} = interdit).` });
|
|
case 'abilities':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` });
|
|
default: return 'Bonus inconnu';
|
|
}
|
|
case 'resistance':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${resistanceTexts[splited[1] as Resistance]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${resistanceTexts[splited[1] as Resistance]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (${resistanceTexts[splited[1] as Resistance]} = interdit).` });
|
|
case 'abilities':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${abilityTexts[splited[1] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${abilityTexts[splited[1] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${abilityTexts[splited[1] as Ability]}.` });
|
|
case 'modifier':
|
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+' }, suffix: { truely: ` au mod. de ${mainStatTexts[splited[1] as MainStat]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Mod. de ${mainStatTexts[splited[1] as MainStat]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Mod. de ${mainStatShortTexts[splited[1] as MainStat]} = interdit).` });
|
|
default: break;
|
|
}
|
|
|
|
return `Inconnu ("${effect.property}")`;
|
|
}
|
|
else if(effect.category === 'list')
|
|
{
|
|
switch(effect.list)
|
|
{
|
|
case 'action':
|
|
case 'reaction':
|
|
case 'freeaction':
|
|
case 'passive':
|
|
return effect.action === 'add' ? getText(effect.item) ?? '' : 'Suppression d\'effet.';
|
|
case 'spells':
|
|
return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item)?.name ?? 'Sort inconnu'}".`;
|
|
case 'sickness':
|
|
return effect.action === 'add' ? `Vous subisez en permanence la maladie "${config.lists.sickness?.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".` : `Vous ne subisez plus la maladie "${config.lists.sickness?.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".`;
|
|
}
|
|
}
|
|
else if(effect.category === 'choice')
|
|
{
|
|
return `${effect.text} (${effect.options?.length ?? 0} options).`;
|
|
}
|
|
|
|
return `Inconnu`;
|
|
}
|
|
function textFromValue(value?: `modifier/${MainStat}` | number | false, settings?: {
|
|
prefix?: { text?: string, positive?: string, negative?: string, truely?: string },
|
|
suffix?: { text?: string, positive?: string, negative?: string, truely?: string },
|
|
falsely?: string
|
|
})
|
|
{
|
|
if(typeof value === 'string')
|
|
return `${settings?.prefix?.truely?.replaceAll('(s)', 's') ?? ''}${settings?.prefix?.text?.replaceAll('(s)', 's') ?? ''}${mainStatShortTexts[value.split('/')[1] as MainStat] ?? 'inconnu'}${settings?.suffix?.text?.replaceAll('(s)', 's') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', 's') ?? ''}`;
|
|
else if(value === false || value === undefined)
|
|
return settings?.falsely ?? '0';
|
|
else if(value >= 0)
|
|
return `${settings?.prefix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.prefix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}`;
|
|
else
|
|
return `${settings?.prefix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.prefix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}`;
|
|
}
|