Feature choice UI rework, feature editor fixes, new character manage page UI with tabgroup and action config
This commit is contained in:
parent
eb0c33deae
commit
3113d8b0f3
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -1,9 +1,9 @@
|
||||||
import { and, eq, SQL, sql, type Operators } from 'drizzle-orm';
|
import { eq, SQL, type Operators } from 'drizzle-orm';
|
||||||
import useDatabase from '~/composables/useDatabase';
|
import useDatabase from '~/composables/useDatabase';
|
||||||
import { characterTable, userPermissionsTable } from '~/db/schema';
|
import { characterTable, userPermissionsTable } from '~/db/schema';
|
||||||
import { hasPermissions } from '~/shared/auth.util';
|
import { hasPermissions } from '~/shared/auth.util';
|
||||||
import { group } from '~/shared/general.util';
|
import { group } from '~/shared/general.util';
|
||||||
import type { Character, Level, MainStat, TrainingLevel } from '~/types/character';
|
import type { Character, MainStat, TrainingLevel } from '~/types/character';
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
|
let { visibility } = getQuery(e) as { visibility?: "public" | "own" | "admin" };
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1423,8 +1423,8 @@ export class CharacterSheet
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
||||||
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
|
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
])) ?? [])
|
])) ?? [])
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
@ -1438,8 +1438,8 @@ export class CharacterSheet
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
||||||
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
|
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
])) ?? [])
|
])) ?? [])
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
@ -1452,8 +1452,8 @@ export class CharacterSheet
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
||||||
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
|
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]),
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
])) ?? [])
|
])) ?? [])
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
@ -1465,8 +1465,8 @@ export class CharacterSheet
|
||||||
return [
|
return [
|
||||||
div('flex flex-col gap-2', [
|
div('flex flex-col gap-2', [
|
||||||
...(character.lists.passive?.map(e => div('flex flex-col gap-1', [
|
...(character.lists.passive?.map(e => div('flex flex-col gap-1', [
|
||||||
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
|
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]),
|
||||||
markdown(getText(e), undefined, { tags: { a: preview } }),
|
markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
|
||||||
])) ?? []),
|
])) ?? []),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,22 @@
|
||||||
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, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
|
||||||
import { div, dom, icon, 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";
|
||||||
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
|
import { button, combobox, foldable, input, multiselect, numberpicker, select, tabgroup, table, toggle, type Option } from "#shared/components.util";
|
||||||
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.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, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
||||||
import characterConfig from "#shared/character-config.json";
|
import characterConfig from "#shared/character-config.json";
|
||||||
import { getID } from "#shared/general.util";
|
import { getID } from "#shared/general.util";
|
||||||
import renderMarkdown, { renderText } from "#shared/markdown.util";
|
import renderMarkdown, { renderText } from "#shared/markdown.util";
|
||||||
import { Tree } from "#shared/tree";
|
import { Tree } from "#shared/tree";
|
||||||
import markdownUtil from "#shared/markdown.util";
|
|
||||||
import { getText } from "#shared/i18n";
|
import { getText } from "#shared/i18n";
|
||||||
|
import markdown from "#shared/markdown.util";
|
||||||
|
|
||||||
const config = characterConfig as CharacterConfig;
|
const config = characterConfig as CharacterConfig;
|
||||||
export class HomebrewBuilder
|
export class HomebrewBuilder
|
||||||
{
|
{
|
||||||
private _container: HTMLDivElement;
|
private _container: HTMLDivElement;
|
||||||
private _content?: HTMLDivElement;
|
private _tabs: HTMLElement & { refresh: () => void };
|
||||||
private _tabsHeader: HTMLDivElement[] = [];
|
|
||||||
private _tabsContent: BuilderTab[] = [];
|
|
||||||
|
|
||||||
private _config: CharacterConfig;
|
private _config: CharacterConfig;
|
||||||
private _editor: FeatureEditor;
|
private _editor: FeatureEditor;
|
||||||
|
|
@ -29,78 +27,21 @@ export class HomebrewBuilder
|
||||||
this._editor = new FeatureEditor();
|
this._editor = new FeatureEditor();
|
||||||
this._container = container;
|
this._container = container;
|
||||||
|
|
||||||
this._tabsHeader = [
|
this._tabs = tabgroup([
|
||||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(0) } }, [text("Peuples")]),
|
{ id: 'peoples', title: [ text("Peuples") ], content: () => this.peoples() },
|
||||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(1) } }, [text("Entrainement")]),
|
{ id: 'training', title: [ text("Entrainement") ], content: () => this.training() },
|
||||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(2) } }, [text("Aspect")]),
|
{ id: 'spells', title: [ text("Sorts") ], content: () => this.spells() },
|
||||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(3) } }, [text("Sorts")]),
|
{ id: 'aspects', title: [ text("Aspects") ], content: () => this.aspects() },
|
||||||
//dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(4) } }, [text("Listes")]),
|
{ id: 'actions', title: [ text("Actions") ], content: () => this.actions() },
|
||||||
];
|
], { focused: 'actions', 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._tabsContent = [
|
|
||||||
new PeopleEditor(this, this._config),
|
this._tabs.children[0]?.appendChild(tooltip(button(icon('radix-icons:clipboard'), () => this.save(), 'p-1'), 'Copier', 'bottom'))
|
||||||
new TrainingEditor(this, this._config),
|
|
||||||
new AspectEditor(this, this._config),
|
|
||||||
new SpellEditor(this, this._config),
|
|
||||||
];
|
|
||||||
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
|
|
||||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||||
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [ div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._tabsHeader), tooltip(button(icon('radix-icons:clipboard-copy', { width: 16, height: 16 }), () => this.save(), 'p-2'), 'Copier le JSON', 'left') ]),
|
this._tabs
|
||||||
this._content,
|
|
||||||
]));
|
]));
|
||||||
|
|
||||||
this.display(1);
|
|
||||||
}
|
}
|
||||||
display(tab: number)
|
peoples()
|
||||||
{
|
{
|
||||||
if(tab < 0 || tab >= this._tabsHeader.length)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this._tabsHeader.forEach(e => e.setAttribute('data-state', 'inactive'));
|
|
||||||
this._tabsHeader[tab]!.setAttribute('data-state', 'active');
|
|
||||||
|
|
||||||
this._content?.replaceChildren(...this._tabsContent[tab]!.dom);
|
|
||||||
}
|
|
||||||
edit(feature: Feature): Promise<Feature>
|
|
||||||
{
|
|
||||||
const promise: Promise<Feature> = this._editor.edit(feature).then(f => {
|
|
||||||
this._config.features[feature.id] = f;
|
|
||||||
return f;
|
|
||||||
}).catch((e) => { if(e) console.error(e); return feature; }).finally(() => {
|
|
||||||
setTimeout(popup.close, 150);
|
|
||||||
this._editor.container.setAttribute('data-state', 'inactive');
|
|
||||||
});
|
|
||||||
const popup = fullblocker([this._editor.container], {
|
|
||||||
priority: true, closeWhenOutside: false,
|
|
||||||
});
|
|
||||||
setTimeout(() => this._editor.container.setAttribute('data-state', 'active'), 1);
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
private save()
|
|
||||||
{
|
|
||||||
navigator.clipboard.writeText(JSON.stringify(this._config));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
abstract class BuilderTab {
|
|
||||||
protected _builder: HomebrewBuilder;
|
|
||||||
protected _config: CharacterConfig;
|
|
||||||
protected _content!: Array<Node | string>;
|
|
||||||
|
|
||||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
||||||
{
|
|
||||||
this._builder = builder;
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
get dom()
|
|
||||||
{
|
|
||||||
return this._content;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
class PeopleEditor extends BuilderTab
|
|
||||||
{
|
|
||||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
||||||
{
|
|
||||||
super(builder, config);
|
|
||||||
|
|
||||||
const add = () => {
|
const add = () => {
|
||||||
const people: RaceConfig = {
|
const people: RaceConfig = {
|
||||||
id: getID(),
|
id: getID(),
|
||||||
|
|
@ -117,23 +58,12 @@ class PeopleEditor extends BuilderTab
|
||||||
}).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<Level, string[]>)
|
}).reduce((p, v) => { p[v[0]] = v[1]; return p }, {} as Record<Level, string[]>)
|
||||||
};
|
};
|
||||||
config.peoples[people.id] = people;
|
config.peoples[people.id] = people;
|
||||||
(this._content[0] as HTMLDivElement).appendChild(peopleRender(people));
|
(content[0] as HTMLDivElement).appendChild(peopleRender(people));
|
||||||
}
|
|
||||||
const remove = (people: RaceConfig) => {
|
|
||||||
confirm('Voulez vous vraiment supprimer cet aspect ?').then(e => {
|
|
||||||
if(e)
|
|
||||||
{
|
|
||||||
Object.values(people.options).forEach(e => e.forEach(id => delete config.features[id]));
|
|
||||||
delete config.peoples[people.id];
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const render = (people: string, level: Level, feature: string) => {
|
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 => {
|
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
|
||||||
this._builder.edit(config.features[feature]!).then(e => {
|
this.edit(config.features[feature]!).then(e => {
|
||||||
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }));
|
element.replaceChildren(markdown(config.features[feature]!.description, undefined, { tags: { a: preview } }));
|
||||||
});
|
});
|
||||||
}, contextmenu: (e) => {
|
}, contextmenu: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -154,7 +84,7 @@ class PeopleEditor extends BuilderTab
|
||||||
}
|
}
|
||||||
}) } } }, [ text('Supprimer') ]) : undefined,
|
}) } } }, [ text('Supprimer') ]) : undefined,
|
||||||
], { placement: "right-start", priority: false });
|
], { placement: "right-start", priority: false });
|
||||||
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]);
|
}}}, [ markdown(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]);
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
const peopleRender = (people: RaceConfig) => {
|
const peopleRender = (people: RaceConfig) => {
|
||||||
|
|
@ -163,24 +93,25 @@ class PeopleEditor extends BuilderTab
|
||||||
]), [ input('text', { defaultValue: people.name, input: (value) => people.name = value, class: 'w-32' }), input('text', { defaultValue: people.description, input: (value) => people.description = value, class: 'w-full' }) ], { class: { container: 'gap-2 max-h-full', title: 'flex flex-row', content: 'flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8' }, open: false })
|
]), [ input('text', { defaultValue: people.name, input: (value) => people.name = value, class: 'w-32' }), input('text', { defaultValue: people.description, input: (value) => people.description = value, class: 'w-full' }) ], { class: { container: 'gap-2 max-h-full', title: 'flex flex-row', content: 'flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8' }, open: false })
|
||||||
}
|
}
|
||||||
const container = div('flex flex-col gap-2', Object.values(config.peoples).map(peopleRender));
|
const container = div('flex flex-col gap-2', Object.values(config.peoples).map(peopleRender));
|
||||||
this._content = [ div('flex flex-col py-2 gap-2', [ div('w-full flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), container ]) ];
|
const content = [ div('flex flex-col py-2 gap-2', [ div('w-full flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), container ]) ];
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
}
|
training()
|
||||||
class TrainingEditor extends BuilderTab
|
|
||||||
{
|
|
||||||
private _options: Record<MainStat, HTMLDivElement[][]>;
|
|
||||||
|
|
||||||
private _tab: number = 0;
|
|
||||||
private _statIndicator: HTMLSpanElement;
|
|
||||||
private _statContainer: HTMLDivElement;
|
|
||||||
|
|
||||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
||||||
{
|
{
|
||||||
super(builder, config);
|
let tab = 0;
|
||||||
|
const switchTab = (tab: number) => {
|
||||||
|
tab = tab;
|
||||||
|
|
||||||
|
_statIndicator.setAttribute('data-text', mainStatTexts[MAIN_STATS[tab] as MainStat]);
|
||||||
|
_statIndicator.style.left = `${tab * 1.5}em`;
|
||||||
|
|
||||||
|
_statContainer.style.left = `-${tab * 100}%`;
|
||||||
|
}
|
||||||
const render = (stat: MainStat, level: TrainingLevel, feature: string) => {
|
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 => {
|
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
|
||||||
this._builder.edit(config.features[feature]!).then(e => {
|
this.edit(config.features[feature]!).then(e => {
|
||||||
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }));
|
element.replaceChildren(markdown(config.features[feature]!.description, undefined, { tags: { a: preview } }));
|
||||||
});
|
});
|
||||||
}, contextmenu: (e) => {
|
}, contextmenu: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -201,7 +132,7 @@ class TrainingEditor extends BuilderTab
|
||||||
}
|
}
|
||||||
}) } } }, [ text('Supprimer') ]) : undefined,
|
}) } } }, [ text('Supprimer') ]) : undefined,
|
||||||
], { placement: "right-start", priority: false });
|
], { placement: "right-start", priority: false });
|
||||||
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]);
|
}}}, [ markdown(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]);
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
const statRenderBlock = (stat: MainStat) => {
|
const statRenderBlock = (stat: MainStat) => {
|
||||||
|
|
@ -211,35 +142,23 @@ class TrainingEditor extends BuilderTab
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
this._options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, HTMLDivElement[][]>);
|
const _options = MAIN_STATS.reduce((p, v) => { p[v] = statRenderBlock(v); return p; }, {} as Record<MainStat, HTMLDivElement[][]>);
|
||||||
|
|
||||||
this._statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' });
|
const _statIndicator = dom('span', { class: 'rounded-full w-3 h-3 bg-accent-blue absolute transition-[left] after:content-[attr(data-text)] after:absolute after:-translate-x-1/2 after:top-4 after:p-px after:bg-light-0 dark:after:bg-dark-0 after:text-center' });
|
||||||
this._statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(this._options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e]))));
|
const _statContainer = div('relative select-none transition-[left] flex flex-1 flex-row max-w-full', Object.values(_options).map(e => div('flex flex-shrink-0 flex-col gap-4 relative w-full overflow-y-auto px-8', e.flatMap(_e => [..._e]))));
|
||||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20", [
|
const content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10 min-h-20", [
|
||||||
div('flex flex-shrink gap-3 items-center relative w-48 ms-12', [
|
div('flex flex-shrink gap-3 items-center relative w-48 ms-12', [
|
||||||
...MAIN_STATS.map((e, i) => dom('span', { listeners: { click: () => this.switchTab(i) }, class: 'block w-2.5 h-2.5 m-px outline outline-1 outline-transparent hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer' })),
|
...MAIN_STATS.map((e, i) => dom('span', { listeners: { click: () => switchTab(i) }, class: 'block w-2.5 h-2.5 m-px outline outline-1 outline-transparent hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer' })),
|
||||||
this._statIndicator,
|
_statIndicator,
|
||||||
]),
|
]),
|
||||||
]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])];
|
]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ _statContainer ])];
|
||||||
|
|
||||||
this.switchTab(0);
|
switchTab(0);
|
||||||
|
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
switchTab(tab: number)
|
aspects()
|
||||||
{
|
{
|
||||||
this._tab = tab;
|
|
||||||
|
|
||||||
this._statIndicator.setAttribute('data-text', mainStatTexts[MAIN_STATS[tab] as MainStat]);
|
|
||||||
this._statIndicator.style.left = `${tab * 1.5}em`;
|
|
||||||
|
|
||||||
this._statContainer.style.left = `-${tab * 100}%`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class AspectEditor extends BuilderTab
|
|
||||||
{
|
|
||||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
||||||
{
|
|
||||||
super(builder, config);
|
|
||||||
|
|
||||||
const render = (aspect: AspectConfig) => {
|
const render = (aspect: AspectConfig) => {
|
||||||
return {
|
return {
|
||||||
name: input('text', { input: (value) => aspect.name = value, defaultValue: aspect.name, class: '!m-0 w-full' }),
|
name: input('text', { input: (value) => aspect.name = value, defaultValue: aspect.name, class: '!m-0 w-full' }),
|
||||||
|
|
@ -255,7 +174,7 @@ class AspectEditor extends BuilderTab
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const add = () => {
|
const add = () => {
|
||||||
config.aspects.push({
|
this._config.aspects.push({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
stat: 'strength',
|
stat: 'strength',
|
||||||
|
|
@ -284,17 +203,12 @@ class AspectEditor extends BuilderTab
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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' } });
|
const redraw = () => table(this._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();
|
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 ] ) ];
|
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
|
||||||
}
|
}
|
||||||
}
|
spells()
|
||||||
class SpellEditor extends BuilderTab
|
|
||||||
{
|
|
||||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
|
||||||
{
|
{
|
||||||
super(builder, config);
|
|
||||||
|
|
||||||
const render = (spell: SpellConfig) => {
|
const render = (spell: SpellConfig) => {
|
||||||
return foldable([
|
return foldable([
|
||||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||||
|
|
@ -302,12 +216,13 @@ class SpellEditor extends BuilderTab
|
||||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
|
||||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||||
|
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('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('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' } }), ]),
|
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.effect = value, defaultValue: spell.effect, class: '!m-0 w-full' }),div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash', { noobserver: true }), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
|
||||||
}
|
}
|
||||||
const add = () => {
|
const add = () => {
|
||||||
config.spells.push({
|
this._config.spells.push({
|
||||||
id: getID(),
|
id: getID(),
|
||||||
name: '',
|
name: '',
|
||||||
rank: 1,
|
rank: 1,
|
||||||
|
|
@ -336,10 +251,68 @@ class SpellEditor extends BuilderTab
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const redraw = () => div('flex flex-col divide-y', config.spells.map(render));
|
const redraw = () => div('flex flex-col divide-y', this._config.spells.map(render));
|
||||||
//, { class: { table: 'flex-1' } });
|
|
||||||
let content = redraw();
|
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 ] ) ];
|
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
|
||||||
|
}
|
||||||
|
actions()
|
||||||
|
{
|
||||||
|
let editing: { type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string } | undefined;
|
||||||
|
const render = (type: 'action' | 'reaction' | 'freeaction' | 'passive', feature: { id: string, name: string, description: string, cost?: number }) => {
|
||||||
|
return div('flex flex-col gap-1', [
|
||||||
|
div('flex flex-row justify-between', [ input('text', { defaultValue: feature.name, input: value => feature.name = value, placeholder: 'Nom', class: '!mx-0 w-80' }), div('flex flex-row gap-2 items-center', [ type === 'action' || type === 'reaction' ? div('flex flex-row items-center', [ numberpicker({ defaultValue: feature?.cost ?? 0, input: value => feature.cost = value, class: '!mx-1' }), text(`point${(feature?.cost ?? 0) > 1 ? 's' : ''}`)]) : undefined, div('flex flex-row items-center gap-2', [ span('text-sm text-light-70 dark:text-dark-70', type), tooltip(button(icon('radix-icons:pencil-1'), () => {
|
||||||
|
|
||||||
|
}, 'p-1'), 'Modifier', 'left'), tooltip(button(icon('radix-icons:trash'), () => remove(type, feature.id), 'p-1'), 'Supprimer', 'right') ]) ])]),
|
||||||
|
markdown(getText(feature.description), undefined, { tags: { a: preview }, class: 'px-2' }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
const add = (type: 'action' | 'reaction' | 'freeaction' | 'passive') => {
|
||||||
|
const feature: { id: string, name: string, description: string, cost?: number } = {
|
||||||
|
id: getID(),
|
||||||
|
name: '',
|
||||||
|
description: getID(),
|
||||||
|
cost: type === 'action' || type === 'reaction' ? 1 : undefined,
|
||||||
|
}
|
||||||
|
this._config[type][feature.id] = feature;
|
||||||
|
|
||||||
|
const element = redraw();
|
||||||
|
content.parentElement?.replaceChild(element, content);
|
||||||
|
content = element;
|
||||||
|
};
|
||||||
|
const remove = (type: 'action' | 'reaction' | 'freeaction' | 'passive', id: string) => {
|
||||||
|
confirm('Voulez vous vraiment supprimer cet effet ?').then(e => {
|
||||||
|
if(e)
|
||||||
|
{
|
||||||
|
delete this._config[type][id];
|
||||||
|
|
||||||
|
const element = redraw();
|
||||||
|
content.replaceWith(element);
|
||||||
|
content = element;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const redraw = () => div('flex flex-col gap-4', [...Object.values(this._config.action).map(e => render('action', e)), ...Object.values(this._config.reaction).map(e => render('reaction', e)), ...Object.values(this._config.freeaction).map(e => render('freeaction', e)), ...Object.values(this._config.passive).map(e => render('passive', e))]);
|
||||||
|
let content = redraw();
|
||||||
|
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), () => add('passive'), 'p-1') ]), content ] ) ];
|
||||||
|
}
|
||||||
|
edit(feature: Feature): Promise<Feature>
|
||||||
|
{
|
||||||
|
const promise: Promise<Feature> = this._editor.edit(feature).then(f => {
|
||||||
|
this._config.features[feature.id] = f;
|
||||||
|
return f;
|
||||||
|
}).catch((e) => { return feature; }).finally(() => {
|
||||||
|
setTimeout(popup.close, 150);
|
||||||
|
this._editor.container.setAttribute('data-state', 'inactive');
|
||||||
|
});
|
||||||
|
const popup = fullblocker([this._editor.container], {
|
||||||
|
priority: true, closeWhenOutside: false,
|
||||||
|
});
|
||||||
|
setTimeout(() => this._editor.container.setAttribute('data-state', 'active'), 1);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
private save()
|
||||||
|
{
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(this._config));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,6 +448,12 @@ export class FeatureEditor
|
||||||
bottom = [ div('px-2 py-1 flex items-center flex-1', [summaryText]) ];
|
bottom = [ div('px-2 py-1 flex items-center flex-1', [summaryText]) ];
|
||||||
break;
|
break;
|
||||||
case 'list':
|
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.action === 'add')
|
||||||
{
|
{
|
||||||
if(buffer.list === 'spells')
|
if(buffer.list === 'spells')
|
||||||
|
|
@ -494,36 +473,48 @@ export class FeatureEditor
|
||||||
{
|
{
|
||||||
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map(e => ({ text: buffer.list !== 'spells' ? renderText(getText((e as Extract<FeatureItem, { category: 'list' }>).item)) : config.spells.find(f => f.id === (e as Extract<FeatureItem, { category: 'list' }>).item)?.name ?? '', value: (e as Extract<FeatureItem, { category: 'list' }>).item })), { defaultValue: buffer.item, change: (item) => buffer.item = item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' }, fill: 'contain' }) ];
|
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map(e => ({ text: buffer.list !== 'spells' ? renderText(getText((e as Extract<FeatureItem, { category: 'list' }>).item)) : config.spells.find(f => f.id === (e as Extract<FeatureItem, { category: 'list' }>).item)?.name ?? '', value: (e as Extract<FeatureItem, { category: 'list' }>).item })), { defaultValue: buffer.item, change: (item) => buffer.item = item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' }, fill: 'contain' }) ];
|
||||||
}
|
}
|
||||||
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => {
|
|
||||||
(buffer as 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' } }) ];
|
|
||||||
break;
|
break;
|
||||||
case 'choice':
|
case 'choice':
|
||||||
const add = () => {
|
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 option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; } = { effects: [{ id: getID() }], text: '' };
|
const addChoice = () => {
|
||||||
(buffer as FeatureChoice).options.push(option as FeatureChoice["options"][number]);
|
const choice: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; } = { effects: [{ id: getID() }], text: '' };
|
||||||
list.appendChild(render(option, true));
|
(buffer as FeatureChoice).options.push(choice as FeatureChoice["options"][number]);
|
||||||
|
return choice;
|
||||||
};
|
};
|
||||||
const render = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; }, state: boolean): HTMLElement => {
|
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 { top: _top, bottom: _bottom } = drawByCategory(option);
|
const _element = renderEffect(option, effect);
|
||||||
const combo = combobox([...featureChoices].filter(e => (e?.value as FeatureItem)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureItem)?.category !== 'choice') : e.value; return e; }), { defaultValue: match(option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => {
|
element.replaceWith(_element);
|
||||||
option = { id: option.id, ...e } as { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; };
|
element = _element;
|
||||||
const element = render(option, true);
|
} }),
|
||||||
_content.replaceWith(element);
|
..._top,
|
||||||
_content = element;
|
]),
|
||||||
} });
|
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") ])
|
||||||
let _content: HTMLElement = foldable(_bottom, [ div('flex flex-1 justify-between', [ div('flex flex-1 flex-row',[ combo, ..._top, input('text', { defaultValue: option.text, input: (value) => option.text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1', placeholder: 'Description' }) ]), tooltip(button(icon('radix-icons:trash'), () => {
|
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-y-auto overflow-x-hidden', _bottom) ]);
|
||||||
_content.remove();
|
|
||||||
(buffer as Extract<FeatureItem, { category: 'choice' }>).options = (buffer as Extract<FeatureItem, { category: 'choice' }>).options.filter(e => e.id !== option.id);
|
return element;
|
||||||
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' }, open: state });
|
|
||||||
return _content; */
|
|
||||||
}
|
}
|
||||||
const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => render(e, false)) ?? []);
|
const renderOption = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[] }, state: boolean) => {
|
||||||
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract<FeatureItem, { category: 'choice' }>).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => add(), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Ajouter une option', 'bottom') ];
|
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 ];
|
bottom = [ list ];
|
||||||
break;
|
break;
|
||||||
default: break;
|
default: break;
|
||||||
|
|
@ -743,7 +734,7 @@ function textFromEffect(effect: Partial<FeatureItem | FeatureEquipment>): string
|
||||||
case 'bonus':
|
case 'bonus':
|
||||||
switch(splited[1])
|
switch(splited[1])
|
||||||
{
|
{
|
||||||
case 'resistance':
|
case 'defense':
|
||||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ` aux jets de résistance de ${resistanceTexts[splited[2] as Resistance]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Jets de résistance de ${resistanceTexts[splited[2] as Resistance]} = ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Résistance ${resistanceTexts[splited[2] as Resistance]} = interdit).` });
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ` aux jets de résistance de ${resistanceTexts[splited[2] as Resistance]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Jets de résistance de ${resistanceTexts[splited[2] as Resistance]} = ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Résistance ${resistanceTexts[splited[2] as Resistance]} = interdit).` });
|
||||||
case 'abilities':
|
case 'abilities':
|
||||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` });
|
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` });
|
||||||
|
|
@ -765,14 +756,17 @@ function textFromEffect(effect: Partial<FeatureItem | FeatureEquipment>): string
|
||||||
switch(effect.list)
|
switch(effect.list)
|
||||||
{
|
{
|
||||||
case 'action':
|
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'}"`;
|
||||||
case 'reaction':
|
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'}"`;
|
||||||
case 'freeaction':
|
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'}"`;
|
||||||
case 'passive':
|
case 'passive':
|
||||||
return effect.action === 'add' ? getText(effect.item) ?? '' : 'Suppression d\'effet.';
|
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'}"`;
|
||||||
case 'spells':
|
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'}".`;
|
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':
|
case 'sickness':
|
||||||
return effect.action === 'add' ? `Vous subisez en permanence la maladie "${config.lists.sickness?.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".` : `Vous ne subisez plus la maladie "${config.lists.sickness?.find(e => e.id === effect.item)?.name ?? 'Maladie inconnue'}".`;
|
return effect.action === 'add' ? `Maladie "${effect.item ? (config.sickness[effect.item]?.name ?? 'inconnue') : 'inconnue'}" permanente.` : `Maladie "${effect.item ? (config.sickness[effect.item]?.name ?? 'inconnue') : 'inconnue'}" supprimée.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(effect.category === 'choice')
|
else if(effect.category === 'choice')
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,11 @@ export type CharacterConfig = {
|
||||||
features: Record<FeatureID, Feature>;
|
features: Record<FeatureID, Feature>;
|
||||||
enchantments: Record<string, EnchantementConfig>; //TODO
|
enchantments: Record<string, EnchantementConfig>; //TODO
|
||||||
items: Record<string, ItemConfig>;
|
items: Record<string, ItemConfig>;
|
||||||
lists: Record<string, { id: string, config: Record<string, any>, values: Record<string, any> }>;
|
sickness: Record<string, { id: string, name: string, description: string, effect: FeatureID[] }>;
|
||||||
|
action: Record<string, { id: string, name: string, description: string, cost: number }>;
|
||||||
|
reaction: Record<string, { id: string, name: string, description: string, cost: number }>;
|
||||||
|
freeaction: Record<string, { id: string, name: string, description: string }>;
|
||||||
|
passive: Record<string, { id: string, name: string, description: string }>;
|
||||||
texts: Record<i18nID, Localized>;
|
texts: Record<i18nID, Localized>;
|
||||||
};
|
};
|
||||||
export type EnchantementConfig = {
|
export type EnchantementConfig = {
|
||||||
|
|
@ -119,6 +123,7 @@ export type SpellConfig = {
|
||||||
elements: Array<SpellElement>;
|
elements: Array<SpellElement>;
|
||||||
effect: string; //TODO -> TextID
|
effect: string; //TODO -> TextID
|
||||||
concentration: boolean;
|
concentration: boolean;
|
||||||
|
range: 'personnal' | number;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
export type RaceConfig = {
|
export type RaceConfig = {
|
||||||
|
|
@ -159,8 +164,7 @@ export type FeatureList = {
|
||||||
category: "list";
|
category: "list";
|
||||||
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
|
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
|
||||||
action: "add" | "remove";
|
action: "add" | "remove";
|
||||||
item: string | i18nID;
|
item: string;
|
||||||
extra?: any;
|
|
||||||
};
|
};
|
||||||
export type FeatureChoice = {
|
export type FeatureChoice = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
|
|
@ -175,7 +179,7 @@ export type FeatureChoice = {
|
||||||
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice;
|
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice;
|
||||||
export type Feature = {
|
export type Feature = {
|
||||||
id: FeatureID;
|
id: FeatureID;
|
||||||
description: i18nID;
|
description: string; //TODO -> TextID
|
||||||
effect: FeatureItem[];
|
effect: FeatureItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue