Aspect and Spell editor, multiselect component.
This commit is contained in:
parent
69ee62c08e
commit
893247e1eb
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -2,7 +2,7 @@ import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter,
|
|||
import { z } from "zod/v4";
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { fakeA } from "#shared/proses";
|
||||
import { button, input, loading, numberpicker, select } from "#shared/components.util";
|
||||
import { button, input, loading, numberpicker, select, toggle } from "#shared/components.util";
|
||||
import { div, dom, icon, mergeClasses, text, type Class } from "#shared/dom.util";
|
||||
import { followermenu, popper } from "#shared/floating.util";
|
||||
import { clamp } from "#shared/general.util";
|
||||
|
|
@ -17,6 +17,7 @@ export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const;
|
|||
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const;
|
||||
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
|
||||
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
|
||||
export const ALIGNMENTS: Alignment[] = [{ kindness: 'good', loyalty: 'loyal' }, { kindness: 'good', loyalty: 'neutral' }, { kindness: 'good', loyalty: 'chaotic' }, { kindness: 'neutral', loyalty: 'loyal' }, { kindness: 'neutral', loyalty: 'neutral' }, { kindness: 'neutral', loyalty: 'chaotic' }, { kindness: 'evil', loyalty: 'loyal' }, { kindness: 'evil', loyalty: 'neutral' }, { kindness: 'evil', loyalty: 'chaotic' }];
|
||||
|
||||
export const defaultCharacter: Character = {
|
||||
id: -1,
|
||||
|
|
@ -715,14 +716,7 @@ class PeoplePicker implements BuilderTab
|
|||
document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`;
|
||||
}
|
||||
});
|
||||
this._visibilityInput = dom("div", { class: `group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, attributes: { "data-state": "unckecked" }, listeners: {
|
||||
click: (e: Event) => {
|
||||
this._builder.character.visibility = this._builder.character.visibility === "private" ? "public" : "private";
|
||||
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
|
||||
}
|
||||
}}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
|
||||
this._visibilityInput = toggle({ defaultValue: this._builder.character.visibility === "private", change: (value) => this._builder.character.visibility = value ? "private" : "public" });
|
||||
|
||||
this._options = config.peoples.map(
|
||||
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
|
|||
return dom('div', { listeners: { click: () => {
|
||||
textValue.textContent = e.text;
|
||||
settings?.change && settings?.change(e.value);
|
||||
close && close();
|
||||
context && context.close && context.close();
|
||||
}, mouseenter: (e) => focus(i) }, class: ['data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text(e.text) ]);
|
||||
});
|
||||
const select = dom('div', { listeners: { click: () => {
|
||||
|
|
@ -96,6 +96,78 @@ export function select<T extends NonNullable<any>>(options: Array<{ text: string
|
|||
})
|
||||
return select;
|
||||
}
|
||||
export function multiselect<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T[], change?: (value: T[]) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||
{
|
||||
let context: { close: Function };
|
||||
let focused: number | undefined;
|
||||
let selection: T[] = settings?.defaultValue ?? [];
|
||||
|
||||
options = options.filter(e => !!e);
|
||||
|
||||
const focus = (i?: number) => {
|
||||
focused !== undefined && optionElements[focused]?.toggleAttribute('data-focused', false);
|
||||
i !== undefined && optionElements[i]?.toggleAttribute('data-focused', true) && optionElements[i]?.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
||||
focused = i;
|
||||
}
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const textValue = text(selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '');
|
||||
const optionElements = options.map((e, i) => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
const element = dom('div', { listeners: { click: () => {
|
||||
selection = selection.includes(e.value) ? selection.filter(f => f !== e.value) : [...selection, e.value];
|
||||
textValue.textContent = selection.length > 0 ? ((options.find(f => f?.value === selection[0])?.text ?? '') + (selection.length > 1 ? ` +${selection.length - 1}` : '')) : '';
|
||||
element.toggleAttribute('data-selected', selection.includes(e.value));
|
||||
settings?.change && settings?.change(selection);
|
||||
context && context.close && context.close();
|
||||
}, mouseenter: (e) => focus(i) }, class: ['group flex flex-row justify-between items-center data-[focused]:bg-light-30 dark:data-[focused]:bg-dark-30 text-light-70 dark:text-dark-70 data-[focused]:text-light-100 dark:data-[focused]:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option], attributes: { 'data-selected': selection.includes(e.value) } }, [ text(e.text), icon('radix-icons:check', { class: 'hidden group-data-[selected]:block', noobserver: true }) ]);
|
||||
return element;
|
||||
});
|
||||
const select = dom('div', { listeners: { click: () => {
|
||||
if(disabled)
|
||||
return;
|
||||
|
||||
const handleKeys = (e: KeyboardEvent) => {
|
||||
switch(e.key.toLocaleLowerCase())
|
||||
{
|
||||
case 'arrowdown':
|
||||
focus(clamp((focused ?? -1) + 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'arrowup':
|
||||
focus(clamp((focused ?? 1) - 1, 0, options.length - 1));
|
||||
return;
|
||||
case 'pageup':
|
||||
focus(0);
|
||||
return;
|
||||
case 'pagedown':
|
||||
focus(optionElements.length - 1);
|
||||
return;
|
||||
case 'enter':
|
||||
focused && optionElements[focused]?.click();
|
||||
return;
|
||||
case 'escape':
|
||||
context?.close();
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeys);
|
||||
|
||||
const box = select.getBoundingClientRect();
|
||||
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => window.removeEventListener('keydown', handleKeys) });
|
||||
} }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(select, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
select.toggleAttribute('data-disabled', disabled);
|
||||
},
|
||||
})
|
||||
return select;
|
||||
}
|
||||
export function combobox<T extends NonNullable<any>>(options: Option<T>[], settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean, fill?: 'contain' | 'cover' }): HTMLElement
|
||||
{
|
||||
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
||||
|
|
@ -294,7 +366,7 @@ export function numberpicker(settings?: { defaultValue?: number, change?: (value
|
|||
focus: () => settings?.focus && settings.focus(),
|
||||
blur: () => settings?.blur && settings.blur(),
|
||||
}});
|
||||
if(settings?.defaultValue) field.value = storedValue.toString(10);
|
||||
if(settings?.defaultValue !== undefined) field.value = storedValue.toString(10);
|
||||
|
||||
return field;
|
||||
}
|
||||
|
|
@ -309,8 +381,23 @@ export function foldable(content: NodeChildren, title: NodeChildren, settings?:
|
|||
return fold;
|
||||
}
|
||||
type TableRow = Record<string, (() => HTMLElement) | HTMLElement | string>;
|
||||
export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class } })
|
||||
export function table(content: TableRow[], headers: TableRow, properties?: { class?: { table?: Class, header?: Class, body?: Class, row?: Class, cell?: Class } })
|
||||
{
|
||||
const render = (item: (() => HTMLElement) | HTMLElement | string) => typeof item === 'string' ? text(item) : typeof item === 'function' ? item() : item;
|
||||
return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: '' }, [ render(e[f]!) ]) : undefined)))) ]);
|
||||
return dom('table', { class: ['', properties?.class?.table] }, [ dom('thead', { class: ['', properties?.class?.header] }, [ dom('tr', { class: '' }, Object.values(headers).map(e => dom('th', {}, [ render(e) ]))) ]), dom('tbody', { class: ['', properties?.class?.body] }, content.map(e => dom('tr', { class: ['', properties?.class?.row] }, Object.keys(headers).map(f => e.hasOwnProperty(f) ? dom('td', { class: ['', properties?.class?.cell] }, [ render(e[f]!) ]) : undefined)))) ]);
|
||||
}
|
||||
export function toggle(settings?: { defaultValue?: boolean, change?: (value: boolean) => void, disabled?: boolean, class?: { container?: Class } })
|
||||
{
|
||||
let state = settings?.defaultValue ?? false;
|
||||
const element = dom("div", { class: [`group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, settings?.class?.container], attributes: { "data-state": state ? "checked" : "unchecked" }, listeners: {
|
||||
click: (e: Event) => {
|
||||
state = !state;
|
||||
element.setAttribute('data-state', state ? "checked" : "unchecked");
|
||||
settings?.change && settings.change(state);
|
||||
}
|
||||
}
|
||||
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
|
||||
return element;
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance } from "~/types/character";
|
||||
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, Resistance, SpellConfig } from "~/types/character";
|
||||
import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util";
|
||||
import { MarkdownEditor } from "#shared/editor.util";
|
||||
import { fakeA } from "#shared/proses";
|
||||
import { button, combobox, foldable, input, numberpicker, select, table, type Option } from "#shared/components.util";
|
||||
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
|
||||
import { fullblocker, tooltip } from "#shared/floating.util";
|
||||
import { elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, spellTypeTexts } from "#shared/character.util";
|
||||
import { ALIGNMENTS, alignmentToString, elementTexts, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
||||
import characterConfig from "#shared/character-config.json";
|
||||
import { clamp, getID, ID_SIZE } from "#shared/general.util";
|
||||
import { getID, ID_SIZE } from "#shared/general.util";
|
||||
import renderMarkdown, { renderText } from "#shared/markdown.util";
|
||||
import { Tree } from "#shared/tree";
|
||||
import markdownUtil from "#shared/markdown.util";
|
||||
|
|
@ -42,8 +42,8 @@ export class HomebrewBuilder
|
|||
new TrainingEditor(this, this._config),
|
||||
new AbilityEditor(this, this._config),
|
||||
new AspectEditor(this, this._config),
|
||||
/* new SpellEditor(this),
|
||||
new ListEditor(this), */
|
||||
new SpellEditor(this, this._config),
|
||||
/* new ListEditor(this), */
|
||||
];
|
||||
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
|
||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||
|
|
@ -173,53 +173,115 @@ class AbilityEditor extends BuilderTab
|
|||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
{
|
||||
super(builder, config);
|
||||
|
||||
Object.entries(config.abilities).map(e => div('flex flex-col gap-4 border border-light-25 dark:border-dark-25', [ ]))
|
||||
|
||||
this._content = [ table(Object.entries(config.abilities).map(e => ({
|
||||
max1: div('', [ text(mainStatTexts[e[1].max[0]]) ]),
|
||||
max2: div('', [ text(mainStatTexts[e[1].max[1]]) ]),
|
||||
name: div('', [ text(e[1].name) ]),
|
||||
description: div('', [ text(e[1].description) ]),
|
||||
id: div('', [ text(e[0]) ]),
|
||||
})), { id: 'ID', name: 'Nom', description: 'Description', max1: 'Stat 1', max2: 'Stat 2' }) ];
|
||||
this._content = [ div('flex px-24 py-4', [table(Object.entries(config.abilities).map(e => ({
|
||||
max1: select(MAIN_STATS.map(e => ({ text: mainStatTexts[e], value: e })), { change: (value) => e[1].max[0] = value, defaultValue: e[1].max[0], class: { container: 'w-full !m-0' } }),
|
||||
max2: select(MAIN_STATS.map(e => ({ text: mainStatTexts[e], value: e })), { change: (value) => e[1].max[1] = value, defaultValue: e[1].max[1], class: { container: 'w-full !m-0' } }),
|
||||
name: input('text', { input: (value) => e[1].name = value, placeholder: 'Nom', defaultValue: e[1].name, class: 'w-full !m-0' }),
|
||||
description: input('text', { input: (value) => e[1].description = value, placeholder: 'Description', defaultValue: e[1].description, class: 'w-full !m-0' }),
|
||||
id: div('w-full !m-0', [ text(e[0]) ]),
|
||||
})), { id: 'ID', name: 'Nom', description: 'Description', max1: 'Stat 1', max2: 'Stat 2' }, { class: { table: 'flex-1' } })] ) ];
|
||||
}
|
||||
}
|
||||
class AspectEditor extends BuilderTab
|
||||
{
|
||||
private _filter: boolean = true;
|
||||
|
||||
private _options: HTMLDivElement[];
|
||||
|
||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
{
|
||||
super(builder, config);
|
||||
|
||||
const render = (aspect: AspectConfig) => {
|
||||
return {
|
||||
name: input('text', { input: (value) => aspect.name = value, defaultValue: aspect.name, class: '!m-0 w-full' }),
|
||||
description: input('text', { input: (value) => aspect.description = value, defaultValue: aspect.description, class: '!m-0 w-full' }),
|
||||
stat: select(MAIN_STATS.map(f => ({ text: mainStatTexts[f], value: f })), { change: (value) => aspect.stat = value, defaultValue: aspect.stat, class: { container: '!m-0 w-full' } }),
|
||||
alignment: select(ALIGNMENTS.map(f => ({ text: alignmentToString(f), value: f })), { change: (value) => aspect.alignment = value, defaultValue: aspect.alignment, class: { container: '!m-0 w-full' } }),
|
||||
magic: toggle({ defaultValue: aspect.magic, change: (value) => aspect.magic = value, class: { container: '' } }),
|
||||
difficulty: numberpicker({ min: 6, max: 13, input: (value) => aspect.difficulty = value, defaultValue: aspect.difficulty, class: '!m-0 w-full' }),
|
||||
physic: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.physic.min, input: (value) => aspect.physic.min = value }), numberpicker({ defaultValue: aspect.physic.max, input: (value) => aspect.physic.max = value }) ]),
|
||||
mental: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.mental.min, input: (value) => aspect.mental.min = value }), numberpicker({ defaultValue: aspect.mental.max, input: (value) => aspect.mental.max = value }) ]),
|
||||
personality: div('flex flex-row justify-center gap-2', [ numberpicker({ defaultValue: aspect.personality.min, input: (value) => aspect.personality.min = value }), numberpicker({ defaultValue: aspect.personality.max, input: (value) => aspect.personality.max = value }) ]),
|
||||
action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:file-text'), () => {}, 'p-1'), button(icon('radix-icons:trash'), () => remove(aspect), 'p-1') ])
|
||||
};
|
||||
}
|
||||
const add = () => {
|
||||
config.aspects.push({
|
||||
name: '',
|
||||
description: '',
|
||||
stat: 'strength',
|
||||
alignment: { kindness: 'good', loyalty: 'loyal' },
|
||||
magic: false,
|
||||
difficulty: 6,
|
||||
physic: { min: 0, max: 30 },
|
||||
mental: { min: 0, max: 20 },
|
||||
personality: { min: 0, max: 20 },
|
||||
options: []
|
||||
});
|
||||
|
||||
/* this._options = config.aspects.map((e, i) => dom('div', { attributes: { "data-aspect": i.toString() }, listeners: { click: () => {
|
||||
this._builder.character.aspect = i;
|
||||
this._options.forEach(_e => _e.setAttribute('data-state', 'inactive'));
|
||||
this._options[i]?.setAttribute('data-state', 'active');
|
||||
}}, class: 'group flex flex-col w-[360px] border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 cursor-pointer' }, [
|
||||
div('bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2 group-data-[state=active]:bg-accent-blue group-data-[state=active]:bg-opacity-10', [
|
||||
div('flex flex-row gap-8 ps-4 items-center', [
|
||||
div("flex flex-1 flex-col gap-2 justify-center", [ div('text-lg font-bold', [ text(e.name) ]), dom('span', { class: 'border-b w-full border-light-50 dark:border-dark-50 group-data-[state=active]:border-b-[4px] group-data-[state=active]:border-accent-blue' }) ]),
|
||||
div('rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10')
|
||||
])
|
||||
]),
|
||||
div('flex justify-stretch items-stretch py-2 px-4 gap-4', [
|
||||
div('flex flex-col flex-1 items-stretch gap-4', [
|
||||
div('flex flex-1 justify-between', [ text('Difficulté'), div('text-sm font-bold', [ text(e.difficulty.toString()) ]) ]),
|
||||
div('flex flex-1 justify-between', [ text('Bonus'), div('text-sm font-bold', [ text(e.stat === 'special' ? 'Special' : mainStatTexts[e.stat]) ]) ])
|
||||
]),
|
||||
div('w-px h-full bg-light-50 dark:bg-dark-50'),
|
||||
div('flex flex-col items-center justify-between py-2', [
|
||||
div('text-sm italic', [ text(alignmentToString(e.alignment)) ]),
|
||||
div(['text-sm font-bold', { "text-light-purple dark:text-dark-purple italic": e.magic, "text-light-orange dark:text-dark-orange": !e.magic }], [ text(e.magic ? 'Magie autorisée' : 'Magie interdite') ]),
|
||||
]),
|
||||
])
|
||||
])); */
|
||||
const element = redraw();
|
||||
content.parentElement?.replaceChild(element, content);
|
||||
content = element;
|
||||
};
|
||||
const remove = (aspect: AspectConfig) => {
|
||||
config.aspects = config.aspects.filter(e => e !== aspect);
|
||||
|
||||
this._content = [ div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', /* this._options */)];
|
||||
const element = redraw();
|
||||
content.parentElement?.replaceChild(element, content);
|
||||
content = element;
|
||||
}
|
||||
const redraw = () => table(config.aspects.map(render), { name: 'Nom', description: 'Description', stat: 'Buff de stat', alignment: 'Alignement', magic: 'Magie', difficulty: 'Difficulté', physic: 'Physique', mental: 'Mental', personality: 'Caractère', action: 'Actions' }, { class: { table: 'flex-1' } });
|
||||
let content = redraw();
|
||||
this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
|
||||
}
|
||||
}
|
||||
class SpellEditor extends BuilderTab
|
||||
{
|
||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
{
|
||||
super(builder, config);
|
||||
|
||||
const render = (spell: SpellConfig) => {
|
||||
return {
|
||||
id: spell.id,
|
||||
name: input('text', { input: (value) => spell.name = value, defaultValue: spell.name, class: '!m-0 w-full' }),
|
||||
rank: select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 w-full' } }),
|
||||
type: select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 w-full' } }),
|
||||
cost: numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }),
|
||||
speed: select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 w-full' } }),
|
||||
elements: multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 w-full' } }),
|
||||
effect: input('text', { input: (value) => spell.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),
|
||||
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 w-full' } }),
|
||||
concentration: toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0' } }),
|
||||
action: div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ])
|
||||
};
|
||||
}
|
||||
const add = () => {
|
||||
config.spells.push({
|
||||
id: getID(ID_SIZE),
|
||||
name: '',
|
||||
rank: 1,
|
||||
type: 'precision',
|
||||
cost: 1,
|
||||
speed: 'action',
|
||||
elements: [],
|
||||
effect: '',
|
||||
concentration: false,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const element = redraw();
|
||||
content.parentElement?.replaceChild(element, content);
|
||||
content = element;
|
||||
};
|
||||
const remove = (spell: SpellConfig) => {
|
||||
config.spells = config.spells.filter(e => e !== spell);
|
||||
|
||||
const element = redraw();
|
||||
content.parentElement?.replaceChild(element, content);
|
||||
content = element;
|
||||
}
|
||||
const redraw = () => table(config.spells.map(render), { id: 'ID', name: 'Nom', rank: 'Rang', type: 'Type', cost: 'Coût', speed: 'Incantation', elements: 'Elements', effect: 'Effet', tags: 'Tag', concentration: 'Concentration', action: 'Actions' }, { class: { table: 'flex-1' } });
|
||||
let content = redraw();
|
||||
this._content = [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export type SpellConfig = {
|
|||
speed: "action" | "reaction" | number;
|
||||
elements: Array<SpellElement>;
|
||||
effect: string;
|
||||
concentration: boolean;
|
||||
tags?: string[];
|
||||
};
|
||||
export type AbilityConfig = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue