Start implementing ItemEditor

This commit is contained in:
Peaceultime 2025-10-13 13:19:50 +02:00
parent 16cc3ee438
commit d187957915
9 changed files with 337 additions and 22 deletions

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -1381,7 +1381,6 @@ export class CharacterSheet
) )
), ),
div("flex flex-row items-center justify-center gap-4", [ div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]), div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50") div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")

View File

@ -594,7 +594,19 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
placement: settings?.position, placement: settings?.position,
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full', class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
content: () => [ content: () => [
settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [ dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }), settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined, settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { useRouter().push(settings.href!); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined, tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { e.stopImmediatePropagation(); floating.hide(); } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined, settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [ dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }), settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined, settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { useRouter().push(settings.href!); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined, tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => {
e.stopImmediatePropagation();
floating.hide();
floating.content.toggleAttribute('data-minimized', false);
minimized && Object.assign(floating.content.style, {
left: `${minimizeBox.left}px`,
top: `${minimizeBox.top}px`,
width: `${minimizeBox.width}px`,
height: `${minimizeBox.height}px`,
});
minimized = false;
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ]) div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
], ],
viewport, viewport,

View File

@ -1,4 +1,4 @@
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character"; import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
import { div, dom, icon, span, text, type NodeChildren } from "#shared/dom.util"; import { div, dom, icon, span, text, type NodeChildren } from "#shared/dom.util";
import { MarkdownEditor } from "#shared/editor.util"; import { MarkdownEditor } from "#shared/editor.util";
import { preview } from "#shared/proses"; import { preview } from "#shared/proses";
@ -18,12 +18,14 @@ export class HomebrewBuilder
private _tabs: HTMLElement & { refresh: () => void }; private _tabs: HTMLElement & { refresh: () => void };
private _config: CharacterConfig; private _config: CharacterConfig;
private _editor: FeatureEditor; private _featureEditor: FeatureEditor;
private _itemEditor: ItemEditor;
constructor(container: HTMLDivElement) constructor(container: HTMLDivElement)
{ {
this._config = config as CharacterConfig; this._config = config as CharacterConfig;
this._editor = new FeatureEditor(); this._featureEditor = new FeatureEditor();
this._itemEditor = new ItemEditor();
this._container = container; this._container = container;
this._tabs = tabgroup([ this._tabs = tabgroup([
@ -32,9 +34,10 @@ export class HomebrewBuilder
{ id: 'spells', title: [ text("Sorts") ], content: () => this.spells() }, { id: 'spells', title: [ text("Sorts") ], content: () => this.spells() },
{ id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() }, { id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() },
{ id: 'actions', title: [ text("Actions") ], content: () => this.actions() }, { id: 'actions', title: [ text("Actions") ], content: () => this.actions() },
{ id: 'items', title: [ text("Objets") ], content: () => this.items() },
], { focused: 'training', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } }); ], { focused: 'training', class: { container: 'flex-1 outline-none max-w-full w-full overflow-y-auto', tabbar: 'flex w-full flex-row gap-4 items-center justify-center relative' } });
this._tabs.children[0]?.appendChild(tooltip(button(icon('radix-icons:clipboard'), () => this.save(), 'p-1'), 'Copier', 'bottom')) this._tabs.children[0]?.appendChild(tooltip(button(icon('radix-icons:clipboard'), () => this.save(), 'p-1'), 'Copier', 'bottom'));
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [ this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
this._tabs this._tabs
])); ]));
@ -352,19 +355,104 @@ export class HomebrewBuilder
const optionHolder = div('flex flex-col gap-4', options.map(e => e.dom)); const optionHolder = div('flex flex-col gap-4', 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: 'Action', click: () => add('action') }, { title: 'Réaction', click: () => add('reaction') }, { title: 'Action libre', click: () => add('freeaction') }, { title: 'Passif', click: () => add('passive') }], { position: 'left-start' }), 'p-1') ]), optionHolder ] ) ]; return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), optionmenu([{ title: 'Action', click: () => add('action') }, { title: 'Réaction', click: () => add('reaction') }, { title: 'Action libre', click: () => add('freeaction') }, { title: 'Passif', click: () => add('passive') }], { position: 'left-start' }), 'p-1') ]), optionHolder ] ) ];
} }
items()
{
type Category = ItemConfig['category'];
type Rarity = ItemConfig['rarity'];
const categoryText: Record<Category, string> = {
'mundane': 'Objet inerte',
'armor': 'Armure',
'weapon': 'Arme',
'wondrous': 'Objet magique'
};
const rarityText: Record<Rarity, string> = {
'common': 'Commun',
'uncommon': 'Peu commun',
'rare': 'Rare',
'legendary': 'Légendaire'
};
const defaultItem = (category: Category): ItemConfig => {
const common: CommonItemConfig = {
id: getID(),
name: '',
description: getID(), // i18nID
rarity: 'common',
equippable: false,
}
switch(category)
{
case 'armor':
return { ...common, category: category, health: 0, absorb: { percent: 0, static: 0 }, type: 'light' };
case 'mundane':
return { ...common, category: category };
case 'weapon':
return { ...common, category: category, damage: '0', type: ['classic'] };
case 'wondrous':
return { ...common, category: category };
}
};
const render = (item: ItemConfig) => {
return {
dom: div('flex flex-col gap-2 border border-light-35 dark:border-dark-35 p-1', [
div('flex flex-row justify-between', [ span('text-xl font-bold px-4', item.name), div('flex flex-row gap-2 items-center', [ div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', categoryText[item.category]), text('-'), span('text-sm text-light-70 dark:text-dark-70', rarityText[item.rarity]), tooltip(button(icon('radix-icons:pencil-1'), () => edit(item), 'p-1'), 'Modifier', 'top'), tooltip(button(icon('radix-icons:trash'), () => remove(item), 'p-1'), 'Supprimer', 'top') ]) ])]),
markdown(getText(item.description), undefined, { tags: { a: preview }, class: 'ms-2 px-2 py-1 border-l-4 border-light-30 dark:border-dark-30' }),
]),
item,
};
};
const add = (category: Category) => {
const item = defaultItem(category);
this._config.texts[item.description!] = { 'fr_FR': '', default: '' };
this._config.items[item.id!] = item;
const option = render(item);
options.push(option);
optionHolder.appendChild(option.dom);
};
const remove = (item: ItemConfig) => {
confirm(`Voulez vous vraiment supprimer l'effet "${item.name}" ?`).then(e => {
if(e)
{
delete this._config.texts[item.description];
delete this._config.items[item.id];
const idx = options.findIndex(e => e.item === item);
options.splice(idx, 1)[0]?.dom.remove();
}
});
};
const edit = (item: ItemConfig) => {
const idx = options.findIndex(e => e.item === item);
this._itemEditor.edit(item).then(f => {
this._config.items[item.id] = f;
options[idx] = render(f);
}).catch((e) => {}).finally(() => {
setTimeout(popup.close, 150);
this._itemEditor.container.setAttribute('data-state', 'inactive');
});
const popup = fullblocker([this._itemEditor.container], {
priority: true, closeWhenOutside: false,
});
setTimeout(() => this._itemEditor.container.setAttribute('data-state', 'active'), 1);
}
const options = Object.values(this._config.items).map(e => render(e));
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> edit(feature: Feature): Promise<Feature>
{ {
const promise: Promise<Feature> = this._editor.edit(feature).then(f => { const promise: Promise<Feature> = this._featureEditor.edit(feature).then(f => {
this._config.features[feature.id] = f; this._config.features[feature.id] = f;
return f; return f;
}).catch((e) => { return feature; }).finally(() => { }).catch((e) => { return feature; }).finally(() => {
setTimeout(popup.close, 150); setTimeout(popup.close, 150);
this._editor.container.setAttribute('data-state', 'inactive'); this._featureEditor.container.setAttribute('data-state', 'inactive');
}); });
const popup = fullblocker([this._editor.container], { const popup = fullblocker([this._featureEditor.container], {
priority: true, closeWhenOutside: false, priority: true, closeWhenOutside: false,
}); });
setTimeout(() => this._editor.container.setAttribute('data-state', 'active'), 1); setTimeout(() => this._featureEditor.container.setAttribute('data-state', 'active'), 1);
return promise; return promise;
} }
private save() private save()
@ -598,6 +686,225 @@ export class FeatureEditor
return this._container; return this._container;
} }
} }
export class ItemEditor
{
private _container: HTMLDivElement;
private _success?: Function;
private _failure?: Function;
private _item?: ItemConfig;
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._item);
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._item);
MarkdownEditor.singleton.onChange = undefined;
}, 'p-1'), 'Annuler', 'left'),
]),
dom('div', { class: 'flex flex-col justify-start items-start my-2 gap-4' }, [
dom('span', { class: 'pb-1 md:p-0 w-full', text: "Description" }),
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(item: ItemConfig): Promise<ItemConfig>
{
return new Promise((success, failure) => {
this._success = success;
this._failure = failure;
this._item = JSON.parse(JSON.stringify(item)) as ItemConfig;
//this._table.replaceChildren(...this._item.effect.map(this._renderEffect.bind(this)));
this._idInput.value = this._item.id;
MarkdownEditor.singleton.onChange = (e) => this._item!.description = e;
MarkdownEditor.singleton.content = this._item.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', [ 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;
} */
get container()
{
return this._container;
}
}
const featureChoices: Option<Partial<FeatureItem>>[] = [ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 1 }, }, { text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 1 }, },

View File

@ -48,7 +48,7 @@ export const a: Prose = {
} }
} }
export const preview: Prose = { export const preview: Prose = {
custom(properties: { href: string, class?: Class, label: string }, children) { custom(properties: { href: string, class?: Class, label: string, events? }, children) {
const href = properties.href as string; const href = properties.href as string;
const { hash, pathname } = parseURL(href); const { hash, pathname } = parseURL(href);
const router = useRouter(); const router = useRouter();
@ -72,7 +72,7 @@ export const preview: Prose = {
queueMicrotask(() => canvas.mount()); queueMicrotask(() => canvas.mount());
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]); return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
} }
return div(''); return div();
})).current], { position: 'bottom-start', pinned: false, })).current], { position: 'bottom-start', pinned: false,
events: { events: {
show: ['mouseenter', 'mousemove'], show: ['mouseenter', 'mousemove'],

15
types/character.d.ts vendored
View File

@ -82,36 +82,33 @@ export type EnchantementConfig = {
export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig); export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig);
type CommonItemConfig = { type CommonItemConfig = {
id: string; id: string;
name: string; //TODO -> TextID
description: i18nID;
rarity: 'common' | 'uncommon' | 'rare' | 'legendary'; rarity: 'common' | 'uncommon' | 'rare' | 'legendary';
weight?: number; //Optionnal but highly recommended weight?: number; //Optionnal but highly recommended
price?: number; //Optionnal but highly recommended price?: number; //Optionnal but highly recommended
power?: number; //Optionnal as most mundane items should not receive enchantments (potions, herbal heals, etc...) power?: number; //Optionnal as most mundane items should not receive enchantments (potions, herbal heals, etc...)
charge?: number //Max amount of charges charge?: number //Max amount of charges
enchantments?: string[]; //Enchantment ID
effects?: Array<FeatureValue | FeatureEquipment | FeatureList>;
equippable: boolean; equippable: boolean;
} }
type ArmorConfig = { type ArmorConfig = {
category: 'armor'; category: 'armor';
name: string; //TODO -> TextID
description: i18nID;
health: number; health: number;
type: 'light' | 'medium' | 'heavy';
absorb: { static: number, percent: number }; absorb: { static: number, percent: number };
}; };
type WeaponConfig = { type WeaponConfig = {
category: 'weapon'; category: 'weapon';
name: string; //TODO -> TextID type: Array<'classic' | 'light' | 'throw' | 'natural' | 'heavy' | 'twohanded' | 'finesse' | 'reach' | 'projectile' | 'shield'>;
description: i18nID;
damage: string; //Dice formula damage: string; //Dice formula
}; };
type WondrousConfig = { type WondrousConfig = {
category: 'wondrous'; category: 'wondrous';
name: string; //TODO -> TextID
description: i18nID;
effect: FeatureItem[];
}; };
type MundaneConfig = { type MundaneConfig = {
category: 'mundane'; category: 'mundane';
name: string; //TODO -> TextID
description: i18nID;
}; };
export type SpellConfig = { export type SpellConfig = {
id: string; id: string;