You've already forked obsidian-visualiser
Feature Builder panel progress
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import { z } from "zod/v4";
|
||||
import characterConfig from './character-config.json';
|
||||
import { button, fakeA, loading } from "./proses";
|
||||
import { button, fakeA, input, loading } from "./proses";
|
||||
import { div, dom, icon, text } from "./dom.util";
|
||||
import { popper } from "./floating.util";
|
||||
import { clamp } from "./general.util";
|
||||
@@ -83,7 +83,6 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
||||
precision: 0,
|
||||
arts: 0,
|
||||
},
|
||||
spells: character.spells ?? [],
|
||||
speed: false,
|
||||
defense: {
|
||||
hardcap: Infinity,
|
||||
@@ -105,8 +104,16 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
||||
magicinstinct: 0,
|
||||
},
|
||||
bonus: {},
|
||||
resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record<MainStat, [number, number]>,
|
||||
resistance: {},
|
||||
initiative: 0,
|
||||
capacity: 0,
|
||||
lists: {
|
||||
action: [],
|
||||
freeaction: [],
|
||||
reaction: [],
|
||||
passive: [],
|
||||
spells: character.spells,
|
||||
},
|
||||
aspect: "",
|
||||
notes: character.notes ?? "",
|
||||
});
|
||||
@@ -120,6 +127,15 @@ export const mainStatTexts: Record<MainStat, string> = {
|
||||
"charisma": "Charisme",
|
||||
"psyche": "Psyché",
|
||||
};
|
||||
export const mainStatShortTexts: Record<MainStat, string> = {
|
||||
"strength": "FOR",
|
||||
"dexterity": "DEX",
|
||||
"constitution": "CON",
|
||||
"intelligence": "INT",
|
||||
"curiosity": "CUR",
|
||||
"charisma": "CHA",
|
||||
"psyche": "PSY",
|
||||
};
|
||||
|
||||
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
|
||||
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
|
||||
@@ -176,7 +192,7 @@ const stepTexts: Record<number, string> = {
|
||||
4: 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.'
|
||||
};
|
||||
|
||||
type Property = { value: number | string, operation: "set" | "add" };
|
||||
type Property = { value: number | string | false, id: string, operation: "set" | "add" };
|
||||
type PropertySum = { list: Array<Property>, value: number, _dirty: boolean };
|
||||
export class CharacterBuilder
|
||||
{
|
||||
@@ -213,15 +229,10 @@ export class CharacterBuilder
|
||||
|
||||
this._result = defaultCompiledCharacter(this._character);
|
||||
|
||||
Object.entries(character.leveling).forEach(e => {
|
||||
const feature = people.options[parseInt(e[0]) as Level][e[1]]!;
|
||||
feature.effect.map(e => this.apply(e));
|
||||
});
|
||||
Object.entries(character.leveling).forEach(e => this.add(people.options[parseInt(e[0]) as Level][e[1]]!));
|
||||
|
||||
MAIN_STATS.forEach(stat => {
|
||||
Object.entries(character.training[stat]).forEach(option => {
|
||||
config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]!.features?.forEach(this.apply.bind(this));
|
||||
})
|
||||
Object.entries(character.training[stat]).forEach(option => this.add(config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]))
|
||||
});
|
||||
}
|
||||
load.remove();
|
||||
@@ -372,7 +383,7 @@ export class CharacterBuilder
|
||||
{
|
||||
if(typeof buffer.list[i]!.value === 'string')
|
||||
{
|
||||
if(this._buffer[buffer.list[i]!.value]!._dirty)
|
||||
if(this._buffer[buffer.list[i]!.value as string]!._dirty)
|
||||
{
|
||||
//Put it back in queue since its dependencies haven't been resolved yet
|
||||
queue.push(property);
|
||||
@@ -381,9 +392,9 @@ export class CharacterBuilder
|
||||
else
|
||||
{
|
||||
if(buffer.list[i]?.operation === 'add')
|
||||
sum += this._buffer[buffer.list[i]!.value]!.value;
|
||||
sum += this._buffer[buffer.list[i]!.value as string]!.value;
|
||||
else if(buffer.list[i]?.operation === 'set')
|
||||
sum = this._buffer[buffer.list[i]!.value]!.value;
|
||||
sum = this._buffer[buffer.list[i]!.value as string]!.value;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -478,31 +489,57 @@ export class CharacterBuilder
|
||||
{
|
||||
if(this._character.training[stat].hasOwnProperty(i))
|
||||
{
|
||||
config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]?.features?.forEach(this.undo.bind(this));
|
||||
this.remove(config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]);
|
||||
delete this._character.training[stat][i as TrainingLevel];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
config.training[stat][level][this._character.training[stat][level]!]?.features?.forEach(this.undo.bind(this));
|
||||
this.remove(config.training[stat][level][this._character.training[stat][level]!]);
|
||||
this._character.training[stat][level] = choice;
|
||||
config.training[stat][level][choice]?.features?.forEach(this.apply.bind(this));
|
||||
this.add(config.training[stat][level][choice]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._character.training[stat][level] = choice;
|
||||
config.training[stat][level][choice]?.features?.forEach(this.apply.bind(this));
|
||||
this.add(config.training[stat][level][choice]);
|
||||
}
|
||||
}
|
||||
private add(feature?: Feature)
|
||||
private add(feature?: string)
|
||||
{
|
||||
feature?.effect.forEach(this.apply.bind(this));
|
||||
if(!feature)
|
||||
return;
|
||||
|
||||
config.features[feature]?.effect.forEach(this.apply.bind(this));
|
||||
}
|
||||
private remove(feature?: Feature)
|
||||
private remove(feature?: string)
|
||||
{
|
||||
feature?.effect.forEach(this.undo.bind(this));
|
||||
if(!feature)
|
||||
return;
|
||||
|
||||
config.features[feature]?.effect.forEach(this.undo.bind(this));
|
||||
}
|
||||
private choose(id: string, choices: number[])
|
||||
{
|
||||
const current = this._character.choices[id];
|
||||
const [ feature, effect ] = id.split('-');
|
||||
const option = config.features[feature!]!.effect.find(e => e.id === effect);
|
||||
|
||||
if(option?.category === 'choice')
|
||||
{
|
||||
if(current !== undefined)
|
||||
{
|
||||
current.forEach(e => this.undo(option.options[e]));
|
||||
}
|
||||
if(choices.length > 0)
|
||||
{
|
||||
choices.forEach(e => this.apply(option.options[e]));
|
||||
}
|
||||
|
||||
this._character.choices[id] = choices;
|
||||
}
|
||||
}
|
||||
private apply(feature?: FeatureItem)
|
||||
{
|
||||
@@ -511,30 +548,26 @@ export class CharacterBuilder
|
||||
|
||||
switch(feature.category)
|
||||
{
|
||||
case "feature":
|
||||
this._result.features[feature.kind] ??= [];
|
||||
|
||||
this._result.features[feature.kind]!.push(feature.text);
|
||||
|
||||
return;
|
||||
case "list":
|
||||
if(feature.action === 'add' && !this._result[feature.list].includes(feature.item))
|
||||
this._result[feature.list].push(feature.item);
|
||||
if(feature.action === 'add' && !this._result.lists[feature.list]!.includes(feature.item))
|
||||
this._result.lists[feature.list]!.push(feature.item);
|
||||
else
|
||||
this._result[feature.list] = this._result[feature.list].filter((e: string) => e !== feature.item);
|
||||
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter((e: string) => e !== feature.item);
|
||||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
|
||||
|
||||
this._buffer[feature.property]!.list.push({ operation: feature.operation, value: feature.value });
|
||||
this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
|
||||
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
|
||||
return;
|
||||
case "choice":
|
||||
const choice = this._character.choices[feature.id]!;
|
||||
choice.forEach(e => this.apply(feature.options[e]!));
|
||||
const choice = this._character.choices[feature.id];
|
||||
|
||||
if(choice)
|
||||
choice.forEach(e => this.apply(feature.options[e]!));
|
||||
|
||||
return;
|
||||
default:
|
||||
@@ -548,29 +581,26 @@ export class CharacterBuilder
|
||||
|
||||
switch(feature.category)
|
||||
{
|
||||
case "feature":
|
||||
this._result.features[feature.kind] = this._result.features[feature.kind]!.filter(e => e !== feature.text);
|
||||
|
||||
return;
|
||||
case "list":
|
||||
if(feature.action === 'remove' && !this._result[feature.list].includes(feature.item))
|
||||
this._result[feature.list].push(feature.item);
|
||||
if(feature.action === 'remove' && !this._result.lists[feature.list]!.includes(feature.item))
|
||||
this._result.lists[feature.list]!.push(feature.item);
|
||||
else
|
||||
this._result[feature.list] = this._result[feature.list].filter(e => e !== feature.item);
|
||||
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter(e => e !== feature.item);
|
||||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
|
||||
|
||||
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.operation === feature.operation && e.value === feature.value), 1);
|
||||
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id), 1);
|
||||
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
|
||||
return;
|
||||
case "choice":
|
||||
const choice = this._character.choices[feature.id]!;
|
||||
choice.forEach(e => this.undo(feature.options[e]!));
|
||||
delete this._character.choices[feature.id];
|
||||
const choice = this._character.choices[feature.id];
|
||||
|
||||
if(choice)
|
||||
choice.forEach(e => this.undo(feature.options[e]!));
|
||||
|
||||
return;
|
||||
default:
|
||||
@@ -599,14 +629,12 @@ class PeoplePicker implements BuilderTab
|
||||
{
|
||||
this._builder = builder;
|
||||
|
||||
this._nameInput = dom("input", { class: `mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
||||
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, listeners: {
|
||||
input: (e: Event) => {
|
||||
this._builder.character.name = this._nameInput.value ?? '';
|
||||
this._nameInput = input('text', {
|
||||
input: (value) => {
|
||||
this._builder.character.name = value ?? '';
|
||||
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: {
|
||||
@@ -709,7 +737,7 @@ class LevelPicker implements BuilderTab
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => dom("div", { class: ["flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px] relative", { 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': (level[0] as any as Level) <= this._builder.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': this._builder.character.leveling[level[0] as any as Level] === j }], listeners: { click: e => {
|
||||
this._builder.toggleLevelOption(parseInt(level[0]) as Level, j);
|
||||
this.update();
|
||||
}}}, [ dom('span', { class: "text-wrap whitespace-pre", text: option.description }), option.effect.some(e => e.category === 'choice') ? div('absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 hover:border-light-70 dark:hover:border-dark-70 flex p-1 justify-center items-center', [ icon('radix-icons:gear') ]) : undefined ])))
|
||||
}}}, [ dom('span', { class: "text-wrap whitespace-pre", text: config.features[option]!.description }), config.features[option]!.effect.some(e => e.category === 'choice') ? div('absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 hover:border-light-70 dark:hover:border-dark-70 flex p-1 justify-center items-center', [ icon('radix-icons:gear') ]) : undefined ])))
|
||||
]);
|
||||
|
||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
|
||||
@@ -788,7 +816,7 @@ class TrainingPicker implements BuilderTab
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => 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.toggleTrainingOption(stat, parseInt(level[0]) as TrainingLevel, j);
|
||||
this.update();
|
||||
}}}, [ markdownUtil(option.description.map(e => e.text).join('\n'), undefined, { tags: { a: fakeA } }) ])))
|
||||
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ])))
|
||||
])
|
||||
}
|
||||
this._builder = builder;
|
||||
|
||||
@@ -704,7 +704,7 @@ export class Editor
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
const close = contextmenu(e.clientX, e.clientY, [
|
||||
const { close } = contextmenu(e.clientX, e.clientY, [
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.add("markdown", item); close() }} }, [icon('radix-icons:plus'), text('Ajouter')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.rename(item); close() }} }, [icon('radix-icons:input'), text('Renommer')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { close(); confirm(`Confirmer la suppression de ${item.title}${item.children ? ' et de ses enfants' : ''} ?`).then(e => { if(e) this.remove(item)}) }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
|
||||
@@ -11,7 +11,7 @@ type Listener<K extends keyof HTMLElementEventMap> = | ((ev: HTMLElementEventMap
|
||||
|
||||
export interface NodeProperties
|
||||
{
|
||||
attributes?: Record<string, string | undefined | boolean>;
|
||||
attributes?: Record<string, string | undefined | boolean | number>;
|
||||
text?: string;
|
||||
class?: Class;
|
||||
style?: Record<string, string | undefined | boolean | number> | string;
|
||||
@@ -30,7 +30,7 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
|
||||
|
||||
if(properties?.attributes)
|
||||
for(const [k, v] of Object.entries(properties.attributes))
|
||||
if(typeof v === 'string') element.setAttribute(k, v);
|
||||
if(typeof v === 'string' || typeof v === 'number') element.setAttribute(k, v.toString(10));
|
||||
else if(typeof v === 'boolean') element.toggleAttribute(k, v);
|
||||
|
||||
if(properties?.text)
|
||||
@@ -113,20 +113,24 @@ export interface IconProperties
|
||||
style?: Record<string, string | undefined> | string;
|
||||
class?: Class;
|
||||
}
|
||||
const iconCache: Map<IconProperties & { name: string }, HTMLElement> = new Map();
|
||||
const iconCache: Map<string, HTMLElement> = new Map();
|
||||
export function icon(name: string, properties?: IconProperties): HTMLElement
|
||||
{
|
||||
const key = { ...properties, name };
|
||||
let el;
|
||||
|
||||
if(iconCache.has(key))
|
||||
return iconCache.get(key)!.cloneNode() as HTMLElement;
|
||||
if(iconCache.has(name))
|
||||
el = iconCache.get(name)!.cloneNode() as HTMLElement;
|
||||
else
|
||||
{
|
||||
el = document.createElement('iconify-icon');
|
||||
|
||||
const el = document.createElement('iconify-icon');
|
||||
if(!iconExists(name))
|
||||
loadIcon(name);
|
||||
|
||||
el.setAttribute('icon', name);
|
||||
|
||||
if(!iconExists(name))
|
||||
loadIcon(name);
|
||||
|
||||
el.setAttribute('icon', name);
|
||||
iconCache.set(name, el.cloneNode() as HTMLElement);
|
||||
}
|
||||
|
||||
properties?.mode && el.setAttribute('mode', properties?.mode.toString());
|
||||
properties?.inline && el.toggleAttribute('inline', properties?.inline);
|
||||
@@ -150,7 +154,6 @@ export function icon(name: string, properties?: IconProperties): HTMLElement
|
||||
for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) el.attributeStyleMap.set(k, v);
|
||||
}
|
||||
|
||||
iconCache.set(key, el.cloneNode() as HTMLElement);
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,12 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||
import { IterMode, Tree } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { dom } from './dom.util';
|
||||
|
||||
const External = Annotation.define<boolean>();
|
||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
||||
const Blockquote = Decoration.line({ class: '*:hidden before:block !ps-4 relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-1 before:bg-none before:bg-light-30 dark:before:bg-dark-30' });
|
||||
|
||||
const TagTag = tags.special(tags.content);
|
||||
|
||||
const intersects = (a: {
|
||||
from: number;
|
||||
to: number;
|
||||
@@ -36,7 +35,6 @@ const highlight = HighlightStyle.define([
|
||||
{ tag: tags.strong, fontWeight: "bold" },
|
||||
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: tags.keyword, color: "#708" },
|
||||
{ tag: TagTag, class: 'cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30' }
|
||||
]);
|
||||
|
||||
class Decorator
|
||||
@@ -111,29 +109,7 @@ export class MarkdownEditor
|
||||
this.view = new EditorView({
|
||||
extensions: [
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
extensions: {
|
||||
defineNodes: [
|
||||
{ name: "Tag", style: TagTag },
|
||||
{ name: "TagMark", style: tags.processingInstruction }
|
||||
],
|
||||
parseInline: [{
|
||||
name: "Tag",
|
||||
parse(cx, next, pos) {
|
||||
if (next != 35 || cx.char(pos + 1) == 35) return -1;
|
||||
let elts = [cx.elt("TagMark", pos, pos + 1)];
|
||||
for (let i = pos + 1; i < cx.end; i++) {
|
||||
let next = cx.char(i);
|
||||
if (next == 35)
|
||||
return cx.addElement(cx.elt("Tag", pos, i + 1, elts.concat(cx.elt("TagMark", i, i + 1))));
|
||||
if (next == 92)
|
||||
elts.push(cx.elt("Escape", i, i++ + 2));
|
||||
if (next == 32 || next == 9 || next == 10 || next == 13) break;
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}],
|
||||
}
|
||||
base: markdownLanguage
|
||||
}),
|
||||
history(),
|
||||
search(),
|
||||
|
||||
318
shared/feature.util.ts
Normal file
318
shared/feature.util.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, TrainingOption } from "~/types/character";
|
||||
import { div, dom, icon, text, type NodeChildren } from "./dom.util";
|
||||
import { MarkdownEditor } from "./editor.util";
|
||||
import { button, combobox, fakeA, input, numberpicker, select } from "./proses";
|
||||
import { popper, tooltip } from "./floating.util";
|
||||
import { mainStatShortTexts, mainStatTexts } from "./character.util";
|
||||
import config from "#shared/character-config.json";
|
||||
import { getID, ID_SIZE } from "./general.util";
|
||||
import renderMarkdown from "./markdown.util";
|
||||
|
||||
export class FeatureEditor
|
||||
{
|
||||
private _container: HTMLDivElement;
|
||||
|
||||
private _success?: Function;
|
||||
private _failure?: Function;
|
||||
private _feature?: Feature;
|
||||
|
||||
private _idInput: HTMLInputElement;
|
||||
private _table: HTMLDivElement;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this._idInput = dom("input", { attributes: { 'disabled': true }, class: `mx-4 text-light-70 dark:text-dark-70 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-25 dark:bg-dark-25 border-light-30 dark:border-dark-30` });
|
||||
this._table = div('grid grid-cols-2 gap-4 px-2');
|
||||
this._container = dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [
|
||||
div('flex flex-row justify-between items-center', [
|
||||
tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => {
|
||||
this._success!(this._feature);
|
||||
MarkdownEditor.singleton.onChange = undefined;
|
||||
}, 'p-1'), 'Valider', 'left'),
|
||||
dom('label', { class: 'flex justify-center items-center my-2' }, [
|
||||
dom('span', { class: 'pb-1 md:p-0', text: "ID" }),
|
||||
this._idInput
|
||||
]),
|
||||
tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => {
|
||||
this._failure!(this._feature);
|
||||
MarkdownEditor.singleton.onChange = undefined;
|
||||
}, 'p-1'), 'Annuler', 'left'),
|
||||
]),
|
||||
dom('span', { class: 'flex flex-col justify-start items-start my-2 gap-4' }, [
|
||||
div('flex w-full items-center justify-between', [
|
||||
dom('span', { class: 'pb-1 md:p-0', text: "Description" }),
|
||||
tooltip(button(icon('radix-icons:clipboard', { width: 20, height: 20 }), () => {
|
||||
MarkdownEditor.singleton.content = this._feature?.effect.map(e => textFromEffect(e)).join('\n') ?? this._feature?.description ?? MarkdownEditor.singleton.content;
|
||||
}, 'p-1'), 'Description automatique', 'left'),
|
||||
]),
|
||||
div('p-1 border border-light-40 dark:border-dark-40 w-full bg-light-25 dark:bg-dark-25 min-h-48 max-h-[32rem]', [ MarkdownEditor.singleton.dom ]),
|
||||
]),
|
||||
div('flex flex-col gap-2 w-full', [
|
||||
div('flex flex-row justify-between', [
|
||||
dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
|
||||
tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => {
|
||||
this._table.appendChild(this._edit({ id: getID(ID_SIZE) }));
|
||||
}, 'p-1'), 'Ajouter', 'left'),
|
||||
]),
|
||||
this._table,
|
||||
])
|
||||
]);
|
||||
}
|
||||
edit(feature: Feature): Promise<Feature>
|
||||
{
|
||||
return new Promise((success, failure) => {
|
||||
this._success = success;
|
||||
this._failure = failure;
|
||||
|
||||
this._feature = JSON.parse(JSON.stringify(feature)) as Feature;
|
||||
|
||||
this._table.replaceChildren(...this._feature.effect.map(this._renderEffect.bind(this)));
|
||||
this._idInput.value = this._feature.id;
|
||||
MarkdownEditor.singleton.onChange = (e) => this._feature!.description = e;
|
||||
MarkdownEditor.singleton.content = this._feature.description;
|
||||
});
|
||||
}
|
||||
private _renderEffect(effect: FeatureItem): HTMLDivElement
|
||||
{
|
||||
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-stretch', [
|
||||
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
|
||||
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
|
||||
this._table.replaceChild(this._edit(effect), content);
|
||||
}, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifier", "bottom"), tooltip(button(icon('radix-icons:trash'), () => {
|
||||
this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id);
|
||||
content.remove();
|
||||
}, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Supprimer", "bottom") ])
|
||||
]) ]);
|
||||
return content;
|
||||
}
|
||||
private _edit(effect: FeatureItem): HTMLDivElement
|
||||
{
|
||||
const match = (effect: FeatureItem): Partial<FeatureItem> | undefined => {
|
||||
switch(effect.category)
|
||||
{
|
||||
case 'value':
|
||||
return choices.find(e => e.value.category === 'value' && e.value.property === effect.property)?.value;
|
||||
/* case 'choice':
|
||||
return choices.find(e => e.value.category === 'choice' && e.value. === effect.property); */
|
||||
case 'list':
|
||||
return choices.find(e => e.value.category === 'list' && e.value.list === effect.list)?.value;
|
||||
}
|
||||
};
|
||||
const approve = () => {
|
||||
const idx = this._feature!.effect.findIndex(e => e.id === buffer.id);
|
||||
|
||||
if(idx === -1)
|
||||
this._feature!.effect.push(buffer);
|
||||
else
|
||||
this._feature!.effect[idx] = buffer;
|
||||
|
||||
this._table.replaceChild(this._renderEffect(buffer), content);
|
||||
}, reject = () => {
|
||||
const idx = this._feature!.effect.findIndex(e => e.id === buffer.id);
|
||||
|
||||
if(idx === -1)
|
||||
content.remove();
|
||||
else
|
||||
this._table.replaceChild(this._renderEffect(effect), content);
|
||||
}
|
||||
let buffer = JSON.parse(JSON.stringify(effect)) as FeatureItem;
|
||||
|
||||
const redraw = () => {
|
||||
let top: NodeChildren = [], bottom: NodeChildren = [];
|
||||
switch(buffer.category)
|
||||
{
|
||||
case 'value':
|
||||
const summaryText = text(textFromEffect(buffer));
|
||||
top = [
|
||||
select([ { text: '+', value: 'add' }, ['speed', 'capacity'].includes(buffer.property) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' } }),
|
||||
typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' }) : 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 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } }),
|
||||
button(icon('radix-icons:update'), () => {
|
||||
(buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
|
||||
const element = redraw();
|
||||
this._table.replaceChild(element, content);
|
||||
content = element;
|
||||
summaryText.textContent = textFromEffect(buffer);
|
||||
}, 'px-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'),
|
||||
];
|
||||
bottom = [summaryText];
|
||||
break;
|
||||
case 'list':
|
||||
if(buffer.action === 'add')
|
||||
{
|
||||
if(buffer.list === 'spells')
|
||||
{
|
||||
bottom = [ combobox(config.spells.map(e => ({ text: e.name, value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' } }) ];
|
||||
}
|
||||
else
|
||||
{
|
||||
const editor = new MarkdownEditor();
|
||||
editor.content = buffer.item;
|
||||
editor.onChange = (item) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = item;
|
||||
|
||||
bottom = [ div('px-4 py-1', [ editor.dom ]) ];
|
||||
}
|
||||
}
|
||||
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove', class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px] w-32' } }) ];
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return div('border border-light-30 dark:border-dark-30 col-span-1 row-span-2', [ div('flex justify-between items-stretch', [
|
||||
div('flex flex-row', [
|
||||
combobox(choices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[250px] -m-px h-[36px]' }, change: (e) => {
|
||||
buffer = { id: buffer.id, ...e } as FeatureItem;
|
||||
const element = redraw();
|
||||
this._table.replaceChild(element, content);
|
||||
content = element;
|
||||
} }),
|
||||
...top,
|
||||
]),
|
||||
div('flex', [ tooltip(button(icon('radix-icons:check'), approve, 'p-2 -m-px 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 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-auto', bottom) ]);
|
||||
}
|
||||
|
||||
let content = redraw();
|
||||
return content;
|
||||
}
|
||||
get container()
|
||||
{
|
||||
return this._container;
|
||||
}
|
||||
}
|
||||
|
||||
const choices: Array<{ text: string, value: Partial<FeatureItem> }> = [
|
||||
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 0 }, },
|
||||
{ text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 0 }, },
|
||||
{ text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 0 }, },
|
||||
{ text: 'Nombre d\'œuvres maitrisés', value: { category: 'value', property: 'artslots', operation: 'add', value: 0 }, },
|
||||
{ text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 0 }, },
|
||||
{ text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 0 }, },
|
||||
{ text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 0 }, },
|
||||
{ text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 0 }, },
|
||||
{ text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 0 }, },
|
||||
{ text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, },
|
||||
{ text: 'Action', value: { category: 'list', list: 'action', action: 'add' }, },
|
||||
{ text: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, },
|
||||
{ text: 'Action libre', value: { category: 'list', list: 'freeaction', action: 'add' }, },
|
||||
{ text: 'Passif', value: { category: 'list', list: 'passive', action: 'add' }, },
|
||||
{ text: 'Choix', value: { category: 'choice', options: [] }, },
|
||||
];
|
||||
function textFromEffect(effect: FeatureItem)
|
||||
{
|
||||
if(effect.category === 'value')
|
||||
{
|
||||
switch(effect.property)
|
||||
{
|
||||
case 'health':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' PV max.' } }) : textFromValue(effect.value, { prefix: { truely: 'PV max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (PV = interdit).' });
|
||||
case 'mana':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' mana max.' } }) : textFromValue(effect.value, { prefix: { truely: 'Mana max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Mana = interdit).' });
|
||||
case 'spellslots':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' sort(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Sorts maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Sorts = interdit).' });
|
||||
case 'artslots':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' œuvre(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Œuvres maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Œuvres = interdit).' });
|
||||
case 'speed':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' case(s) de course.' }, falsely: '+0 cases de course' }) : textFromValue(effect.value, { prefix: { truely: 'Vitesse de course de ' }, suffix: { truely: ' case(s).' }, falsely: 'Déplacement impossible.' });
|
||||
case 'capacity':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' unité(s) d\'quipement.' } }) : textFromValue(effect.value, { prefix: { truely: 'Capacité d\'equipement fixé à ' }, suffix: { truely: ' unité(s).' }, falsely: 'Impossible de posséder du materiel.' });
|
||||
case 'initiative':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' à l\'itiniative.' } }) : textFromValue(effect.value, { prefix: { truely: 'Initiative fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Initiative = interdit).' });
|
||||
case 'training':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) d\'entrainement.' } }) : `Opération interdite (Entrainement fixe).`;
|
||||
case 'ability':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) de compétence.' } }) : `Opération interdite (Compétences fixe).`;
|
||||
default: break;
|
||||
}
|
||||
|
||||
const splited = effect.property.split('/');
|
||||
switch(splited[0])
|
||||
{
|
||||
case 'spellranks':
|
||||
return '';
|
||||
case 'defense':
|
||||
switch(splited[1])
|
||||
{
|
||||
case 'hardcap':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Défense max ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Défense max fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Hardcap = interdit).' });
|
||||
case 'static':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Base de défense ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Base de défense fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Static = interdit).' });
|
||||
case 'activeparry':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active parry = interdit).' });
|
||||
case 'activedodge':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active dodge = interdit).' });
|
||||
case 'passiveparry':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive parry = interdit).' });
|
||||
case 'passivedodge':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive dodge = interdit).' });
|
||||
default: return 'Défense inconnue.';
|
||||
}
|
||||
case 'mastery':
|
||||
switch(splited[1])
|
||||
{
|
||||
case 'strength':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise for = interdit).' });
|
||||
case 'dexterity':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise dex = interdit).' });
|
||||
case 'shield':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise boucliers = interdit).' });
|
||||
case 'armor':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise armure = interdit).' });
|
||||
case 'multiattack':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Attaque multiple ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Attaque multiple fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Attaque multiple = interdit).' });
|
||||
case 'magicpower':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise puissance = interdit).' });
|
||||
case 'magicspeed':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise rapidité = interdit).' });
|
||||
case 'magicelement':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise elements = interdit).' });
|
||||
case 'magicinstinct':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise instinct = interdit).' });
|
||||
default: return 'Maitrise inconnue.';
|
||||
}
|
||||
/* case 'resistance':
|
||||
return splited[1] ? config.resistances[splited[1] as string].name : 'résistance inconnue'; */
|
||||
case 'abilities':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${`${config.abilities[splited[1] as Ability].name}.`}` });
|
||||
case 'modifier':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+' }, suffix: { truely: ` au mod. de ${mainStatTexts[splited[1] as MainStat]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Mod. de ${mainStatTexts[splited[1] as MainStat]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Mod. de ${mainStatShortTexts[splited[1] as MainStat]} = interdit).` });
|
||||
default: break;
|
||||
}
|
||||
|
||||
return `Inconnu ("${effect.property}")`;
|
||||
}
|
||||
else if(effect.category === 'list')
|
||||
{
|
||||
switch(effect.list)
|
||||
{
|
||||
case 'action':
|
||||
case 'reaction':
|
||||
case 'freeaction':
|
||||
case 'passive':
|
||||
return effect.action === 'add' ? effect.item : 'Suppression d\'effet.';
|
||||
case 'spells':
|
||||
return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item) ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item) ?? 'Sort inconnu'}".`;
|
||||
}
|
||||
}
|
||||
else if(effect.category === 'choice')
|
||||
{
|
||||
return `Choix (WIP)`;
|
||||
}
|
||||
else
|
||||
{
|
||||
return `Inconnu`;
|
||||
}
|
||||
}
|
||||
function textFromValue(value: `modifier/${MainStat}` | number | false, settings?: {
|
||||
prefix?: { text?: string, positive?: string, negative?: string, truely?: string },
|
||||
suffix?: { text?: string, positive?: string, negative?: string, truely?: string },
|
||||
falsely?: string
|
||||
})
|
||||
{
|
||||
if(typeof value === 'string')
|
||||
return `${settings?.prefix?.truely?.replaceAll('(s)', 's') ?? ''}${settings?.prefix?.text?.replaceAll('(s)', 's') ?? ''}${mainStatShortTexts[value.split('/')[1] as MainStat] ?? 'inconnu'}${settings?.suffix?.text?.replaceAll('(s)', 's') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', 's') ?? ''}`;
|
||||
else if(value === false)
|
||||
return settings?.falsely ?? '0';
|
||||
else if(value >= 0)
|
||||
return `${settings?.prefix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.prefix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}`;
|
||||
else
|
||||
return `${settings?.prefix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.prefix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}`;
|
||||
}
|
||||
@@ -8,12 +8,13 @@ export interface ContextProperties
|
||||
offset?: number;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
style?: Record<string, string | undefined | boolean | number> | string;
|
||||
viewport?: HTMLElement;
|
||||
}
|
||||
export interface PopperProperties extends ContextProperties
|
||||
{
|
||||
content?: NodeChildren;
|
||||
delay?: number;
|
||||
viewport?: HTMLElement;
|
||||
|
||||
onShow?: (element: HTMLDivElement) => boolean | void;
|
||||
onHide?: (element: HTMLDivElement) => boolean | void;
|
||||
@@ -36,7 +37,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
||||
{
|
||||
let shown = false, timeout: Timer;
|
||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]);
|
||||
const content = dom('div', { class: ['fixed hidden', properties?.class], attributes: { 'data-state': 'closed' } }, [...(properties?.content ?? []), arrow]);
|
||||
const content = dom('div', { class: ['fixed hidden', properties?.class], style: properties?.style, attributes: { 'data-state': 'closed' } }, [...(properties?.content ?? []), arrow]);
|
||||
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
||||
|
||||
function update()
|
||||
@@ -151,7 +152,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
||||
|
||||
return container;
|
||||
}
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: ContextProperties): () => void
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: ContextProperties & { blur?: () => void })
|
||||
{
|
||||
const virtual = {
|
||||
getBoundingClientRect() {
|
||||
@@ -167,9 +168,10 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
||||
};
|
||||
},
|
||||
};
|
||||
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
||||
|
||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class] }, content);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class], style: properties?.style }, content);
|
||||
|
||||
function update()
|
||||
{
|
||||
@@ -178,9 +180,10 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
||||
strategy: 'fixed',
|
||||
middleware: [
|
||||
properties?.offset ? FloatingUI.offset(properties?.offset) : undefined,
|
||||
FloatingUI.flip(),
|
||||
properties?.offset ? FloatingUI.shift({ padding: properties?.offset }) : undefined,
|
||||
FloatingUI.shift({ rootBoundary: rect }),
|
||||
properties?.offset ? FloatingUI.shift({ padding: properties?.offset, rootBoundary: rect }) : undefined,
|
||||
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
|
||||
FloatingUI.hide({ rootBoundary: rect }),
|
||||
]
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(container.style, {
|
||||
@@ -242,16 +245,27 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
||||
|
||||
container.remove();
|
||||
stop();
|
||||
properties?.blur && properties.blur();
|
||||
}
|
||||
|
||||
return close;
|
||||
return { close, container, content };
|
||||
}
|
||||
export function tooltip(container: HTMLElement, txt: string, placement: FloatingUI.Placement, delay?: number): HTMLElement
|
||||
{
|
||||
return popper(container, {
|
||||
arrow: true,
|
||||
offset: 8,
|
||||
delay: delay,
|
||||
content: [ text(txt) ],
|
||||
placement: placement,
|
||||
class: "fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50"
|
||||
});
|
||||
}
|
||||
|
||||
export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||
export function fullblocker(content: NodeChildren, properties?: ModalProperties)
|
||||
{
|
||||
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } });
|
||||
const _closer = properties?.priority ? undefined : dom('span', { class: 'absolute top-4 right-4', text: '×', listeners: { click: () => _modal.remove() } });
|
||||
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content)])
|
||||
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);
|
||||
|
||||
teleport.appendChild(_modal);
|
||||
|
||||
@@ -259,6 +273,10 @@ export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||
close: () => _modal.remove(),
|
||||
}
|
||||
}
|
||||
export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||
{
|
||||
return fullblocker([ dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content) ], properties);
|
||||
}
|
||||
|
||||
export function confirm(title: string): Promise<boolean>
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ export function unifySlug(slug: string | string[]): string
|
||||
export function getID(length: number)
|
||||
{
|
||||
for (var id = [], i = 0; i < length; i++)
|
||||
id.push((16 * Math.random() | 0).toString(16));
|
||||
id.push((36 * Math.random() | 0).toString(36));
|
||||
return id.join("");
|
||||
}
|
||||
export function group<
|
||||
|
||||
196
shared/proses.ts
196
shared/proses.ts
@@ -1,11 +1,11 @@
|
||||
import { dom, icon, type NodeChildren, type Node, type NodeProperties, type Class, mergeClasses } from "#shared/dom.util";
|
||||
import { dom, icon, type NodeChildren, type Node, type NodeProperties, type Class, mergeClasses, text, div } from "#shared/dom.util";
|
||||
import { parseURL } from 'ufo';
|
||||
import render from "#shared/markdown.util";
|
||||
import { popper } from "#shared/floating.util";
|
||||
import { contextmenu, popper } from "#shared/floating.util";
|
||||
import { Canvas } from "#shared/canvas.util";
|
||||
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
||||
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
|
||||
import { unifySlug } from "#shared/general.util";
|
||||
import { clamp, unifySlug } from "#shared/general.util";
|
||||
|
||||
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
||||
export type Prose = { class: string } | { custom: CustomProse };
|
||||
@@ -230,4 +230,194 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
|
||||
return dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
|
||||
}
|
||||
export function select<T extends NonNullable<any>>(options: Array<string | undefined> | Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||
{
|
||||
const textFromValue = (value?: T): string => {
|
||||
if(!value)
|
||||
return '';
|
||||
const found = options.find(e => (e as { value: string } | undefined)?.value === value || e === value);
|
||||
if(!found)
|
||||
return '';
|
||||
return (found as { text: string } | undefined)?.text ?? found as string;
|
||||
};
|
||||
let close: Function | undefined;
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const textValue = text(textFromValue(settings?.defaultValue));
|
||||
const optionElements = options.map(e => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
return dom('div', { listeners: { click: () => {
|
||||
let text, value;
|
||||
if(typeof e === 'string')
|
||||
{
|
||||
text = value = e;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = e.text;
|
||||
value = e.value;
|
||||
}
|
||||
|
||||
textValue.textContent = text;
|
||||
settings?.change && settings?.change(value);
|
||||
close && close();
|
||||
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text((e as { text: string } | undefined)?.text ?? e as string) ]);
|
||||
});
|
||||
const select = dom('div', { listeners: { click: () => {
|
||||
if(disabled)
|
||||
return;
|
||||
|
||||
const box = select.getBoundingClientRect();
|
||||
close = 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` } }).close;
|
||||
} }, 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: Array<string | undefined> | Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||
{
|
||||
const textFromValue = (value?: T): string => {
|
||||
if(!value)
|
||||
return '';
|
||||
const found = options.find(e => (e as { value: string } | undefined)?.value === value || e === value);
|
||||
if(!found)
|
||||
return '';
|
||||
return (found as { text: string } | undefined)?.text ?? found as string;
|
||||
};
|
||||
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
||||
let selected = true;
|
||||
|
||||
const show = () => {
|
||||
if(disabled || (context && context.container.parentElement))
|
||||
return;
|
||||
|
||||
const box = container.getBoundingClientRect();
|
||||
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements.map(e => e?.dom) : [ 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: () => { if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red') } });
|
||||
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
|
||||
};
|
||||
const hide = () => {
|
||||
if(!context || !context.container.parentElement)
|
||||
return;
|
||||
|
||||
context.close();
|
||||
if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red');
|
||||
};
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const optionElements = options.map((e, i) => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
return { item: e, dom: dom('div', { listeners: { click: () => {
|
||||
let text, value;
|
||||
if(typeof e === 'string')
|
||||
{
|
||||
text = value = e;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = e.text;
|
||||
value = e.value;
|
||||
}
|
||||
|
||||
select.value = text;
|
||||
settings?.change && settings?.change(value);
|
||||
selected = true;
|
||||
hide();
|
||||
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text((e as { text: string } | undefined)?.text ?? e as string) ]) };
|
||||
});
|
||||
const select = dom('input', { listeners: { focus: show, input: () => {
|
||||
context && context?.container.replaceChildren(...optionElements.filter(e => {
|
||||
if(e === undefined)
|
||||
return false;
|
||||
if(typeof e.item === 'string')
|
||||
return (e.item as string).toLowerCase().includes(select.value.toLowerCase());
|
||||
return e.item.text.toLowerCase().includes(select.value.toLowerCase());
|
||||
}).map(e => e!.dom));
|
||||
selected = false;
|
||||
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
|
||||
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
|
||||
select.value = textFromValue(settings?.defaultValue);
|
||||
|
||||
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 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] }, [ select, icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(container, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
container.toggleAttribute('data-disabled', disabled);
|
||||
select.toggleAttribute('disabled', disabled);
|
||||
},
|
||||
})
|
||||
return container;
|
||||
}
|
||||
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean }): HTMLInputElement
|
||||
{
|
||||
const input = dom("input", { attributes: { disabled: settings?.disabled }, class: [`mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
||||
bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
|
||||
input: () => settings?.input && settings.input(input.value),
|
||||
change: () => settings?.change && settings.change(input.value),
|
||||
focus: () => settings?.focus,
|
||||
blur: () => settings?.blur,
|
||||
}})
|
||||
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
|
||||
|
||||
return input;
|
||||
}
|
||||
export function numberpicker(settings?: { defaultValue?: number, change?: (value: number) => void, input?: (value: number) => void, focus?: () => void, blur?: () => void, class?: Class, min?: number, max?: number, disabled?: boolean }): HTMLInputElement
|
||||
{
|
||||
let storedValue = settings?.defaultValue ?? 0;
|
||||
const validateAndChange = (value: number) => {
|
||||
if(isNaN(value))
|
||||
field.value = '';
|
||||
else
|
||||
{
|
||||
value = clamp(value, settings?.min ?? -Infinity, settings?.max ?? Infinity);
|
||||
field.value = value.toString(10);
|
||||
if(storedValue !== value)
|
||||
{
|
||||
storedValue = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const field = dom("input", { attributes: { disabled: settings?.disabled }, class: [`w-14 mx-4 caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50 bg-light-20 dark:bg-dark-20 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20`, settings?.class], listeners: {
|
||||
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
||||
keydown: (e: KeyboardEvent) => {
|
||||
switch(e.key)
|
||||
{
|
||||
case "ArrowUp":
|
||||
validateAndChange(storedValue + (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
validateAndChange(storedValue - (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "PageUp":
|
||||
settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "PageDown":
|
||||
settings?.min && validateAndChange(settings.min) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
change: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.change && settings.change(storedValue),
|
||||
focus: () => settings?.focus && settings.focus(),
|
||||
blur: () => settings?.blur && settings.blur(),
|
||||
}});
|
||||
if(settings?.defaultValue) field.value = storedValue.toString(10);
|
||||
|
||||
return field;
|
||||
}
|
||||
Reference in New Issue
Block a user