|
|
|
|
@@ -1,10 +1,10 @@
|
|
|
|
|
import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
|
|
|
|
|
import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponType } from "~/types/character";
|
|
|
|
|
import { div, dom, icon, span, text, type NodeChildren } 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, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
|
|
|
|
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, damageTypeTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, 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";
|
|
|
|
|
@@ -78,9 +78,10 @@ export class HomebrewBuilder
|
|
|
|
|
}
|
|
|
|
|
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.edit(config.features[feature]!).then(e => {
|
|
|
|
|
FeaturePanel.edit(config.features[feature]!).then(e => {
|
|
|
|
|
config.features[feature] = e;
|
|
|
|
|
element.replaceChildren(markdown(config.features[feature]!.description, undefined, { tags: { a: preview } }));
|
|
|
|
|
});
|
|
|
|
|
}).catch(e => {});
|
|
|
|
|
}, contextmenu: (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const context = contextmenu(e.clientX, e.clientY, [
|
|
|
|
|
@@ -126,9 +127,10 @@ export class HomebrewBuilder
|
|
|
|
|
}
|
|
|
|
|
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.edit(config.features[feature]!).then(e => {
|
|
|
|
|
FeaturePanel.edit(config.features[feature]!).then(e => {
|
|
|
|
|
config.features[feature] = e;
|
|
|
|
|
element.replaceChildren(markdown(config.features[feature]!.description, undefined, { tags: { a: preview } }));
|
|
|
|
|
});
|
|
|
|
|
}).catch(e => {});
|
|
|
|
|
}, contextmenu: (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const context = contextmenu(e.clientX, e.clientY, [
|
|
|
|
|
@@ -235,7 +237,7 @@ export class HomebrewBuilder
|
|
|
|
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Portée'), select<'personnal' | number>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { change: (value) => spell.range = value, defaultValue: spell.range, 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 });
|
|
|
|
|
], [ 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.description = value, defaultValue: spell.description, 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 = () => {
|
|
|
|
|
this._config.spells.push({
|
|
|
|
|
@@ -246,7 +248,7 @@ export class HomebrewBuilder
|
|
|
|
|
cost: 1,
|
|
|
|
|
speed: 'action',
|
|
|
|
|
elements: [],
|
|
|
|
|
effect: '',
|
|
|
|
|
description: '',
|
|
|
|
|
concentration: false,
|
|
|
|
|
range: 0,
|
|
|
|
|
tags: [],
|
|
|
|
|
@@ -385,7 +387,7 @@ export class HomebrewBuilder
|
|
|
|
|
case 'armor':
|
|
|
|
|
return { ...common, category: category, health: 0, absorb: { percent: 0, static: 0 }, type: 'light' };
|
|
|
|
|
case 'weapon':
|
|
|
|
|
return { ...common, category: category, damage: '0', type: ['classic'] };
|
|
|
|
|
return { ...common, category: category, damage: { type: 'slashing', value: '0' }, type: ['classic'] };
|
|
|
|
|
case 'wondrous':
|
|
|
|
|
case 'mundane':
|
|
|
|
|
return { ...common, category: category };
|
|
|
|
|
@@ -435,54 +437,259 @@ export class HomebrewBuilder
|
|
|
|
|
const optionHolder = div('grid grid-cols-3 gap-2', options.map(e => e.dom));
|
|
|
|
|
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), optionmenu([{ title: 'Objet inerte', click: () => add('mundane') }, { title: 'Armure', click: () => add('armor') }, { title: 'Arme', click: () => add('weapon') }, { title: 'Objet magique', click: () => add('wondrous') }], { position: 'left-start' }), 'p-1') ]), optionHolder ] ) ];
|
|
|
|
|
}
|
|
|
|
|
edit(feature: Feature): Promise<Feature>
|
|
|
|
|
{
|
|
|
|
|
const promise: Promise<Feature> = this._featureEditor.edit(feature).then(f => {
|
|
|
|
|
this._config.features[feature.id] = f;
|
|
|
|
|
return f;
|
|
|
|
|
}).catch((e) => { return feature; }).finally(() => {
|
|
|
|
|
setTimeout(popup.close, 150);
|
|
|
|
|
this._featureEditor.container.setAttribute('data-state', 'inactive');
|
|
|
|
|
});
|
|
|
|
|
const popup = fullblocker([this._featureEditor.container], {
|
|
|
|
|
priority: true, closeWhenOutside: false,
|
|
|
|
|
});
|
|
|
|
|
setTimeout(() => this._featureEditor.container.setAttribute('data-state', 'active'), 1);
|
|
|
|
|
return promise;
|
|
|
|
|
}
|
|
|
|
|
private save()
|
|
|
|
|
{
|
|
|
|
|
navigator.clipboard.writeText(JSON.stringify(this._config));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type FeatureOption = Partial<FeatureValue | FeatureEquipment | FeatureList | FeatureChoice> & { id: string };
|
|
|
|
|
class FeatureEditor
|
|
|
|
|
{
|
|
|
|
|
private _list: Record<string, FeatureOption> | FeatureOption[];
|
|
|
|
|
private _id: string;
|
|
|
|
|
private _draft: boolean;
|
|
|
|
|
|
|
|
|
|
private _arr: boolean;
|
|
|
|
|
private option!: FeatureOption;
|
|
|
|
|
|
|
|
|
|
container!: HTMLElement;
|
|
|
|
|
|
|
|
|
|
constructor(list: Record<string, FeatureOption> | FeatureOption[], id: string, draft: boolean)
|
|
|
|
|
{
|
|
|
|
|
this._arr = Array.isArray(list);
|
|
|
|
|
this._list = list;
|
|
|
|
|
this._id = id;
|
|
|
|
|
|
|
|
|
|
if(this._arr ? !(list as FeatureOption[]).find(e => e.id === id) : !list.hasOwnProperty(id))
|
|
|
|
|
throw new Error();
|
|
|
|
|
|
|
|
|
|
this.read();
|
|
|
|
|
this._draft = draft;
|
|
|
|
|
|
|
|
|
|
this.container = div();
|
|
|
|
|
|
|
|
|
|
if(draft)
|
|
|
|
|
this.edit();
|
|
|
|
|
else
|
|
|
|
|
this.show();
|
|
|
|
|
}
|
|
|
|
|
private read()
|
|
|
|
|
{
|
|
|
|
|
this.option = JSON.parse(JSON.stringify(this._arr ? (this._list as FeatureOption[]).find(e => e.id === this._id)! : (this._list as Record<string, FeatureOption>)[this._id]!));
|
|
|
|
|
}
|
|
|
|
|
private update()
|
|
|
|
|
{
|
|
|
|
|
if(this._arr)
|
|
|
|
|
{
|
|
|
|
|
const idx = (this._list as FeatureOption[]).findIndex(e => e.id === this.option.id);
|
|
|
|
|
|
|
|
|
|
if(idx === -1)
|
|
|
|
|
throw new Error();
|
|
|
|
|
|
|
|
|
|
(this._list as FeatureOption[])[idx]! = this.option;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
(this._list as Record<string, FeatureOption>)[this.option.id] = this.option;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private delete()
|
|
|
|
|
{
|
|
|
|
|
if(this._arr)
|
|
|
|
|
{
|
|
|
|
|
const idx = (this._list as FeatureOption[]).findIndex(e => e.id === this.option.id);
|
|
|
|
|
|
|
|
|
|
if(idx === -1)
|
|
|
|
|
throw new Error();
|
|
|
|
|
|
|
|
|
|
(this._list as FeatureOption[]).splice(idx, 1);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
delete (this._list as Record<string, FeatureOption>)[this.option.id];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private show()
|
|
|
|
|
{
|
|
|
|
|
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', [ markdown(textFromEffect(this.option), undefined, { tags: { a: preview } }) ]),
|
|
|
|
|
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => this.edit(), '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.delete();
|
|
|
|
|
this.container.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") ])
|
|
|
|
|
]) ]);
|
|
|
|
|
|
|
|
|
|
this.container.replaceWith(content);
|
|
|
|
|
this.container = content;
|
|
|
|
|
}
|
|
|
|
|
private static match(effect: FeatureOption): Partial<FeatureOption> | 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private editByCategory(buffer: FeatureOption)
|
|
|
|
|
{
|
|
|
|
|
let top: NodeChildren = [], bottom: NodeChildren = [];
|
|
|
|
|
switch(this.option.category)
|
|
|
|
|
{
|
|
|
|
|
case 'value':
|
|
|
|
|
return this.editValue(buffer as Partial<FeatureValue>);
|
|
|
|
|
case 'list':
|
|
|
|
|
return this.editList(buffer as Partial<FeatureList>);
|
|
|
|
|
case 'choice':
|
|
|
|
|
return this.editChoice(buffer as Partial<FeatureChoice>);
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
return { top, bottom };
|
|
|
|
|
}
|
|
|
|
|
private editValue(buffer: Partial<FeatureValue | FeatureEquipment>)
|
|
|
|
|
{
|
|
|
|
|
const valueVariable = () => typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { buffer.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.value = value; summaryText.textContent = textFromEffect(buffer); } });
|
|
|
|
|
const summaryText = text(textFromEffect(buffer));
|
|
|
|
|
let valueSelection = valueVariable();
|
|
|
|
|
|
|
|
|
|
return { top: [
|
|
|
|
|
select([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].includes(buffer.property ?? '') || ['defense/'].some(e => buffer.property?.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { buffer.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.value = (typeof buffer.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])
|
|
|
|
|
] };
|
|
|
|
|
}
|
|
|
|
|
private editList(buffer: Partial<FeatureList>)
|
|
|
|
|
{
|
|
|
|
|
let list: Option<string>[];
|
|
|
|
|
if(buffer.action === 'add')
|
|
|
|
|
{
|
|
|
|
|
if(buffer.list === 'spells')
|
|
|
|
|
{
|
|
|
|
|
list = 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)
|
|
|
|
|
{
|
|
|
|
|
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) => e.list === 'spells' ? config.spells.find(f => f.id === e.item)! : config[e.list][e.item]!).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 }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
top: [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => {
|
|
|
|
|
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' }) ]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private editChoice(buffer: Partial<FeatureChoice>)
|
|
|
|
|
{
|
|
|
|
|
const availableChoices: Option<Partial<FeatureValue | FeatureList>>[] = featureChoices.filter(e => (e?.value as FeatureOption)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureOption)?.category !== 'choice') : e.value; return e; }) as Option<Partial<FeatureValue | FeatureList>>[];
|
|
|
|
|
const addChoice = () => {
|
|
|
|
|
const choice: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; } = { effects: [{ id: getID() }], text: '' };
|
|
|
|
|
buffer.options ??= [];
|
|
|
|
|
buffer.options.push(choice as FeatureChoice["options"][number]);
|
|
|
|
|
return choice;
|
|
|
|
|
};
|
|
|
|
|
const addEffect = (choice: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }) => {
|
|
|
|
|
const effect: (Partial<FeatureValue | FeatureList>) = { id: getID() };
|
|
|
|
|
choice.effects.push(effect);
|
|
|
|
|
return effect;
|
|
|
|
|
};
|
|
|
|
|
const renderEffect = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, effect: Partial<FeatureValue | FeatureList>) => {
|
|
|
|
|
const { top: _top, bottom: _bottom } = this.editByCategory(effect as FeatureOption);
|
|
|
|
|
let element = 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(availableChoices, { defaultValue: FeatureEditor.match(effect as FeatureOption) as Partial<FeatureValue | FeatureList> | undefined, class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e: Partial<FeatureValue | FeatureList>) => {
|
|
|
|
|
const idx = option.effects.findIndex(e => e === effect);
|
|
|
|
|
option.effects[idx] = effect = { ...e, id: effect.id };
|
|
|
|
|
|
|
|
|
|
const _element = renderEffect(option, effect);
|
|
|
|
|
element.replaceWith(_element);
|
|
|
|
|
element = _element;
|
|
|
|
|
} }),
|
|
|
|
|
..._top,
|
|
|
|
|
]),
|
|
|
|
|
div('flex', [ tooltip(button(icon('radix-icons:trash'), () => { option.effects = option.effects.filter(e => e === effect); element.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") ])
|
|
|
|
|
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', _bottom) ]);
|
|
|
|
|
|
|
|
|
|
return element;
|
|
|
|
|
}
|
|
|
|
|
const renderOption = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, state: boolean) => {
|
|
|
|
|
const effects = div('flex flex-col -m-px flex flex-col ms-px ps-8 w-full', option.effects.map(e => renderEffect(option, e)));
|
|
|
|
|
let _content = foldable([ effects ], [ div('flex flex-row flex-1 justify-between', [ input('text', { defaultValue: option.text, input: (value) => option.text = value, placeholder: 'Nom de l\'option', class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1' }), div('flex flex-row flex-shrink-1', [ tooltip(button(icon('radix-icons:plus'), () => effects.appendChild(renderEffect(option, addEffect(option))), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvel effet', 'bottom'), , tooltip(button(icon('radix-icons:trash'), () => {
|
|
|
|
|
_content.remove();
|
|
|
|
|
buffer.options?.splice(buffer.options.findIndex(e => e !== option), 1);
|
|
|
|
|
}, '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 => renderOption(e, false)) ?? []);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
top: [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as FeatureChoice).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'), () => list.appendChild(renderOption(addChoice(), true)), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvelle option', 'bottom') ],
|
|
|
|
|
bottom: [ list ],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private edit()
|
|
|
|
|
{
|
|
|
|
|
const redraw = () => {
|
|
|
|
|
const { top, bottom } = this.editByCategory(this.option);
|
|
|
|
|
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: FeatureEditor.match(this.option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => {
|
|
|
|
|
this.option = { id: this.option.id, ...e } as FeatureOption;
|
|
|
|
|
content = redraw();
|
|
|
|
|
|
|
|
|
|
this.container.replaceWith(content);
|
|
|
|
|
this.container = content;
|
|
|
|
|
} }),
|
|
|
|
|
...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) ]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let content = redraw();
|
|
|
|
|
|
|
|
|
|
this.container.replaceWith(content);
|
|
|
|
|
this.container = content;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class FeaturePanel
|
|
|
|
|
{
|
|
|
|
|
private _container: HTMLDivElement;
|
|
|
|
|
|
|
|
|
|
private _success?: Function;
|
|
|
|
|
private _failure?: Function;
|
|
|
|
|
private _feature?: Feature;
|
|
|
|
|
|
|
|
|
|
private _idInput: HTMLInputElement;
|
|
|
|
|
private _table: HTMLDivElement;
|
|
|
|
|
|
|
|
|
|
constructor()
|
|
|
|
|
static render(feature: Feature, success: (feature: Feature) => void, failure: (feature: Feature) => void)
|
|
|
|
|
{
|
|
|
|
|
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]' }, [
|
|
|
|
|
const _feature = JSON.parse(JSON.stringify(feature)) as Feature;
|
|
|
|
|
const effectContainer = div('grid grid-cols-2 gap-4 px-2', _feature.effect.map(e => new FeatureEditor(_feature.effect!, e.id, false).container));
|
|
|
|
|
MarkdownEditor.singleton.content = getText(_feature.description);
|
|
|
|
|
MarkdownEditor.singleton.onChange = (value) => ItemPanel.config.texts[_feature.description]!.default = value;
|
|
|
|
|
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 }), () => {
|
|
|
|
|
this._success!(this._feature);
|
|
|
|
|
success!(_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
|
|
|
|
|
input("text", { defaultValue: _feature.id, 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` })
|
|
|
|
|
]),
|
|
|
|
|
tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => {
|
|
|
|
|
this._failure!(this._feature);
|
|
|
|
|
failure!(feature);
|
|
|
|
|
MarkdownEditor.singleton.onChange = undefined;
|
|
|
|
|
}, 'p-1'), 'Annuler', 'left'),
|
|
|
|
|
]),
|
|
|
|
|
@@ -490,8 +697,8 @@ export class FeaturePanel
|
|
|
|
|
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;
|
|
|
|
|
MarkdownEditor.singleton.content = _feature?.effect.map(e => textFromEffect(e)).join('\n') ?? _feature?.description ?? MarkdownEditor.singleton.content;
|
|
|
|
|
if(_feature?.description) _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 ]),
|
|
|
|
|
@@ -500,186 +707,30 @@ export class FeaturePanel
|
|
|
|
|
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() }));
|
|
|
|
|
const f = { id: getID(), };
|
|
|
|
|
//@ts-expect-error
|
|
|
|
|
_feature.effect.push(f);
|
|
|
|
|
effectContainer.appendChild(new FeatureEditor(_feature.effect, f.id, true).container);
|
|
|
|
|
}, 'p-1'), 'Ajouter', 'left'),
|
|
|
|
|
]),
|
|
|
|
|
this._table,
|
|
|
|
|
effectContainer,
|
|
|
|
|
])
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
edit(feature: Feature): Promise<Feature>
|
|
|
|
|
static 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;
|
|
|
|
|
let container: HTMLElement, close: Function;
|
|
|
|
|
return new Promise<Feature>((success, failure) => {
|
|
|
|
|
container = FeaturePanel.render(feature, 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');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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', [ markdown(textFromEffect(effect), undefined, { tags: { a: preview } }) ]),
|
|
|
|
|
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 FeatureValue).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 FeatureValue).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 FeatureValue).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as FeatureValue).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 FeatureValue).value = (typeof (buffer as FeatureValue).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':
|
|
|
|
|
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => {
|
|
|
|
|
(buffer as FeatureList).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' } }) ];
|
|
|
|
|
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(renderMDAsText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).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 if(buffer.list)
|
|
|
|
|
{
|
|
|
|
|
bottom = [ combobox(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 })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).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
|
|
|
|
|
{
|
|
|
|
|
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map((e) => (e as FeatureList).list === 'spells' ? config.spells.find(f => f.id === (e as FeatureList).item) : config[(e as FeatureList).list][(e as FeatureList).item]).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 })), { 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' }) ];
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'choice':
|
|
|
|
|
const availableChoices: Option<Partial<FeatureValue | FeatureList>>[] = 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; }) as Option<Partial<FeatureValue | FeatureList>>[];
|
|
|
|
|
const addChoice = () => {
|
|
|
|
|
const choice: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; } = { effects: [{ id: getID() }], text: '' };
|
|
|
|
|
(buffer as FeatureChoice).options.push(choice as FeatureChoice["options"][number]);
|
|
|
|
|
return choice;
|
|
|
|
|
};
|
|
|
|
|
const addEffect = (choice: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }) => {
|
|
|
|
|
const effect: (Partial<FeatureValue | FeatureList>) = { id: getID() };
|
|
|
|
|
choice.effects.push(effect);
|
|
|
|
|
return effect;
|
|
|
|
|
};
|
|
|
|
|
const renderEffect = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, effect: Partial<FeatureValue | FeatureList>) => {
|
|
|
|
|
const { top: _top, bottom: _bottom } = drawByCategory(effect);
|
|
|
|
|
let element = 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(availableChoices, { defaultValue: match(effect as FeatureItem) as Partial<FeatureValue | FeatureList> | undefined, class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e: Partial<FeatureValue | FeatureList>) => {
|
|
|
|
|
const idx = option.effects.findIndex(e => e === effect);
|
|
|
|
|
option.effects[idx] = effect = { ...e, id: effect.id };
|
|
|
|
|
|
|
|
|
|
const _element = renderEffect(option, effect);
|
|
|
|
|
element.replaceWith(_element);
|
|
|
|
|
element = _element;
|
|
|
|
|
} }),
|
|
|
|
|
..._top,
|
|
|
|
|
]),
|
|
|
|
|
div('flex', [ tooltip(button(icon('radix-icons:trash'), () => { option.effects = option.effects.filter(e => e === effect); element.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") ])
|
|
|
|
|
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', _bottom) ]);
|
|
|
|
|
|
|
|
|
|
return element;
|
|
|
|
|
}
|
|
|
|
|
const renderOption = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, state: boolean) => {
|
|
|
|
|
const effects = div('flex flex-col -m-px flex flex-col ms-px ps-8 w-full', option.effects.map(e => renderEffect(option, e)));
|
|
|
|
|
let _content = foldable([ effects ], [ div('flex flex-row flex-1 justify-between', [ input('text', { defaultValue: option.text, input: (value) => option.text = value, placeholder: 'Nom de l\'option', class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1' }), div('flex flex-row flex-shrink-1', [ tooltip(button(icon('radix-icons:plus'), () => effects.appendChild(renderEffect(option, addEffect(option))), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvel effet', 'bottom'), , tooltip(button(icon('radix-icons:trash'), () => {
|
|
|
|
|
_content.remove();
|
|
|
|
|
(buffer as FeatureChoice).options = (buffer as FeatureChoice).options.filter(e => e !== option);
|
|
|
|
|
}, '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 => renderOption(e, false)) ?? []);
|
|
|
|
|
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as FeatureChoice).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'), () => list.appendChild(renderOption(addChoice(), true)), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvelle 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export class ItemPanel
|
|
|
|
|
{
|
|
|
|
|
@@ -689,6 +740,7 @@ export class ItemPanel
|
|
|
|
|
const _item = JSON.parse(JSON.stringify(item)) as ItemConfig;
|
|
|
|
|
MarkdownEditor.singleton.content = getText(_item.description);
|
|
|
|
|
MarkdownEditor.singleton.onChange = (value) => ItemPanel.config.texts[_item.description]!.default = value;
|
|
|
|
|
const effectContainer = div('grid grid-cols-2 gap-4 px-2 flex-1', _item.effects?.map(e => new FeatureEditor(_item.effects!, 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 }), () => {
|
|
|
|
|
@@ -709,14 +761,28 @@ export class ItemPanel
|
|
|
|
|
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', [ checkbox({ defaultValue: _item.consummable, change: function(value) { _item.consummable = value; this.parentElement?.toggleAttribute('data-disabled', !value) }, class: { container: '!w-4 !h-4' } }), span('', 'Consommable'), ]) ]),
|
|
|
|
|
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'), ]) ]),
|
|
|
|
|
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ checkbox({ defaultValue: _item.charge !== undefined, change: function(value) { _item.charge = 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('', 'Charges'), ]), numberpicker({ defaultValue: _item.charge, disabled: _item.charge === undefined, input: (v) => _item.charge = v, class: '!w-1/3' }), ]),
|
|
|
|
|
], [ 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: _item.rarity, change: (v) => _item.rarity = v }), ]) ], { 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 } ),
|
|
|
|
|
], [ 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: _item.rarity, change: (v) => _item.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 } ),
|
|
|
|
|
_item.category === 'armor' ? foldable([
|
|
|
|
|
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Type'), ]), select<'light' | 'medium' | 'heavy'>([{ text: 'Armure légère', value: 'light' }, { text: 'Armure moyenne', value: 'medium' }, { text: 'Armure lourde', value: 'heavy' }], { defaultValue: _item.type, change: (v) => _item.type = v, class: { container: '!w-1/2' } }), ]),
|
|
|
|
|
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Durabilité'), ]), numberpicker({ defaultValue: _item.health, input: (v) => _item.health = v, class: '!w-1/3' }), ]),
|
|
|
|
|
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Absorbtion (fixe)'), ]), numberpicker({ defaultValue: _item.absorb.static, input: (v) => _item.absorb.static = v, class: '!w-1/3' }), ]),
|
|
|
|
|
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Absorbtion (%)'), ]), numberpicker({ defaultValue: _item.absorb.percent, input: (v) => _item.absorb.percent = v, class: '!w-1/3' }), ]),
|
|
|
|
|
], [ span('text-lg font-bold', "Armure") ], { 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 } ) : undefined,
|
|
|
|
|
_item.category === 'weapon' ? foldable([
|
|
|
|
|
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Type de dégâts'), ]), select(Object.keys(damageTypeTexts).map(e => ({ text: damageTypeTexts[e as DamageType], value: e as DamageType })), { defaultValue: _item.damage.type, change: (v) => _item.damage.type = v, class: { container: '!w-1/3' } }), ]),
|
|
|
|
|
div('flex flex-row gap-2 items-center justify-between', [ div('flex flex-row gap-2 items-center', [ span('', 'Dégats'), ]), input('text', { defaultValue: _item.damage.value, input: (v) => _item.damage.value = v, class: '!w-1/3' }), ]),
|
|
|
|
|
], [ 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é'), ]), multiselect(Object.keys(weaponTypeTexts).map(e => ({ text: weaponTypeTexts[e as WeaponType], value: e as WeaponType })), { defaultValue: _item.type, change: (v) => _item.type = 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 } ) : undefined,
|
|
|
|
|
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]', [ MarkdownEditor.singleton.dom ])], [ span('text-lg font-bold px-2', "Description") ], { class: { container: 'gap-4 pb-2 border-b border-light-35 dark:border-dark-35' }, open: true, }),
|
|
|
|
|
foldable([ div('grid grid-cols-2 gap-4 px-2'), ], [ dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
|
|
|
|
|
foldable([ effectContainer ], [ 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'),
|
|
|
|
|
const f = { id: getID(), };
|
|
|
|
|
_item.effects ??= [];
|
|
|
|
|
//@ts-expect-error
|
|
|
|
|
_item.effects.push(f);
|
|
|
|
|
effectContainer.appendChild(new FeatureEditor(_item.effects, 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' } })
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
@@ -734,164 +800,9 @@ export class ItemPanel
|
|
|
|
|
container.setAttribute('data-state', 'inactive');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/* 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', [ markdown(textFromEffect(effect), undefined, { tags: { a: preview } }) ]),
|
|
|
|
|
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._item!.effect = this._item!.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._item!.effect.findIndex(e => e.id === _buffer.id);
|
|
|
|
|
|
|
|
|
|
if(idx === -1)
|
|
|
|
|
this._item!.effect.push(_buffer);
|
|
|
|
|
else
|
|
|
|
|
this._item!.effect[idx] = _buffer;
|
|
|
|
|
|
|
|
|
|
this._table.replaceChild(this._renderEffect(_buffer), content);
|
|
|
|
|
}, reject = () => {
|
|
|
|
|
const idx = this._item!.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 FeatureValue).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 FeatureValue).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 FeatureValue).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as FeatureValue).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 FeatureValue).value = (typeof (buffer as FeatureValue).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':
|
|
|
|
|
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => {
|
|
|
|
|
(buffer as FeatureList).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' } }) ];
|
|
|
|
|
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(renderMDAsText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).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 if(buffer.list)
|
|
|
|
|
{
|
|
|
|
|
bottom = [ combobox(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 })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).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
|
|
|
|
|
{
|
|
|
|
|
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map((e) => (e as FeatureList).list === 'spells' ? config.spells.find(f => f.id === (e as FeatureList).item) : config[(e as FeatureList).list][(e as FeatureList).item]).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 })), { 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' }) ];
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'choice':
|
|
|
|
|
const availableChoices: Option<Partial<FeatureValue | FeatureList>>[] = 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; }) as Option<Partial<FeatureValue | FeatureList>>[];
|
|
|
|
|
const addChoice = () => {
|
|
|
|
|
const choice: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; } = { effects: [{ id: getID() }], text: '' };
|
|
|
|
|
(buffer as FeatureChoice).options.push(choice as FeatureChoice["options"][number]);
|
|
|
|
|
return choice;
|
|
|
|
|
};
|
|
|
|
|
const addEffect = (choice: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }) => {
|
|
|
|
|
const effect: (Partial<FeatureValue | FeatureList>) = { id: getID() };
|
|
|
|
|
choice.effects.push(effect);
|
|
|
|
|
return effect;
|
|
|
|
|
};
|
|
|
|
|
const renderEffect = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, effect: Partial<FeatureValue | FeatureList>) => {
|
|
|
|
|
const { top: _top, bottom: _bottom } = drawByCategory(effect);
|
|
|
|
|
let element = 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(availableChoices, { defaultValue: match(effect as FeatureItem) as Partial<FeatureValue | FeatureList> | undefined, class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e: Partial<FeatureValue | FeatureList>) => {
|
|
|
|
|
const idx = option.effects.findIndex(e => e === effect);
|
|
|
|
|
option.effects[idx] = effect = { ...e, id: effect.id };
|
|
|
|
|
|
|
|
|
|
const _element = renderEffect(option, effect);
|
|
|
|
|
element.replaceWith(_element);
|
|
|
|
|
element = _element;
|
|
|
|
|
} }),
|
|
|
|
|
..._top,
|
|
|
|
|
]),
|
|
|
|
|
div('flex', [ tooltip(button(icon('radix-icons:trash'), () => { option.effects = option.effects.filter(e => e === effect); element.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") ])
|
|
|
|
|
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', _bottom) ]);
|
|
|
|
|
|
|
|
|
|
return element;
|
|
|
|
|
}
|
|
|
|
|
const renderOption = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, state: boolean) => {
|
|
|
|
|
const effects = div('flex flex-col -m-px flex flex-col ms-px ps-8 w-full', option.effects.map(e => renderEffect(option, e)));
|
|
|
|
|
let _content = foldable([ effects ], [ div('flex flex-row flex-1 justify-between', [ input('text', { defaultValue: option.text, input: (value) => option.text = value, placeholder: 'Nom de l\'option', class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1' }), div('flex flex-row flex-shrink-1', [ tooltip(button(icon('radix-icons:plus'), () => effects.appendChild(renderEffect(option, addEffect(option))), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvel effet', 'bottom'), , tooltip(button(icon('radix-icons:trash'), () => {
|
|
|
|
|
_content.remove();
|
|
|
|
|
(buffer as FeatureChoice).options = (buffer as FeatureChoice).options.filter(e => e !== option);
|
|
|
|
|
}, '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 => renderOption(e, false)) ?? []);
|
|
|
|
|
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as FeatureChoice).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'), () => list.appendChild(renderOption(addChoice(), true)), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Nouvelle 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;
|
|
|
|
|
} */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const featureChoices: Option<Partial<FeatureItem>>[] = [
|
|
|
|
|
const featureChoices: Option<Partial<FeatureOption>>[] = [
|
|
|
|
|
{ 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 }, },
|
|
|
|
|
@@ -979,8 +890,8 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
|
|
|
|
|
{ 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 | FeatureEquipment>): string
|
|
|
|
|
const flattenFeatureChoices = Tree.accumulate(featureChoices, 'value', (item) => Array.isArray(item.value) ? undefined : item.value).filter(e => !!e) as Partial<FeatureOption>[];
|
|
|
|
|
function textFromEffect(effect: Partial<FeatureOption>): string
|
|
|
|
|
{
|
|
|
|
|
if(effect.category === 'value')
|
|
|
|
|
{
|
|
|
|
|
@@ -1000,7 +911,7 @@ function textFromEffect(effect: Partial<FeatureItem | FeatureEquipment>): string
|
|
|
|
|
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.' });
|
|
|
|
|
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\'équipement 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':
|
|
|
|
|
@@ -1101,13 +1012,13 @@ function textFromEffect(effect: Partial<FeatureItem | FeatureEquipment>): string
|
|
|
|
|
switch(effect.list)
|
|
|
|
|
{
|
|
|
|
|
case 'action':
|
|
|
|
|
return effect.action === 'add' ? effect.item ? getText((config.action[effect.item]?.description) ?? 'Inconnu') : 'Inconnu' : `Suppression de l'action "${effect.item ? (config.action[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
return effect.action === 'add' ? `Gain de l'action "${effect.item ? (config.action[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression de l'action "${effect.item ? (config.action[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
case 'reaction':
|
|
|
|
|
return effect.action === 'add' ? effect.item ? getText((config.reaction[effect.item]?.description ?? 'Inconnu')) : 'Inconnu' : `Suppression de la réaction "${effect.item ? (config.reaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
return effect.action === 'add' ? `Gain de la réaction "${effect.item ? (config.reaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression de la réaction "${effect.item ? (config.reaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
case 'freeaction':
|
|
|
|
|
return effect.action === 'add' ? effect.item ? getText((config.freeaction[effect.item]?.description ?? 'Inconnu')) : 'Inconnu' : `Suppression de l'action libre "${effect.item ? (config.freeaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
return effect.action === 'add' ? `Gain de l'action libre "${effect.item ? (config.freeaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression de l'action libre "${effect.item ? (config.freeaction[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
case 'passive':
|
|
|
|
|
return effect.action === 'add' ? effect.item ? getText((config.passive[effect.item]?.description ?? 'Inconnu')) : 'Inconnu' : `Suppression du passif "${effect.item ? (config.passive[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
return effect.action === 'add' ? `Gain du passif "${effect.item ? (config.passive[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"` : `Suppression du passif "${effect.item ? (config.passive[effect.item]?.name ?? 'Inconnu') : 'Inconnu'}"`;
|
|
|
|
|
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':
|
|
|
|
|
|