Impoved FloatingUI components and create a PickableFeature class
This commit is contained in:
parent
4e5ea504ea
commit
72982a4ea9
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,68 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { FeatureEditor } from '~/shared/feature.util';
|
||||
import { confirm, fullblocker } from '~/shared/floating.util';
|
||||
import { getID, ID_SIZE } from '~/shared/general.util';
|
||||
import type { CharacterConfig, Feature } from '~/types/character';
|
||||
import { HomebrewBuilder } from '~/shared/feature.util';
|
||||
|
||||
//@ts-ignore
|
||||
const config = ref<CharacterConfig>(characterConfig);
|
||||
const featureEditor = new FeatureEditor();
|
||||
|
||||
function copy()
|
||||
{
|
||||
navigator.clipboard.writeText(JSON.stringify(config.value));
|
||||
}
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
});
|
||||
|
||||
function createFeature()
|
||||
{
|
||||
const feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
|
||||
const container = useTemplateRef('container');
|
||||
|
||||
featureEditor.edit(feature).then(feature => {
|
||||
config.value.features[feature.id] = feature;
|
||||
}).catch(() => {}).finally(() => {
|
||||
setTimeout(popup.close, 150);
|
||||
featureEditor.container.setAttribute('data-state', 'inactive');
|
||||
});
|
||||
|
||||
const popup = fullblocker([featureEditor.container], {
|
||||
priority: true, closeWhenOutside: false,
|
||||
});
|
||||
featureEditor.container.setAttribute('data-state', 'active');
|
||||
}
|
||||
function editFeature(id: string)
|
||||
{
|
||||
config.value.features[id] && featureEditor.edit(config.value.features[id]).then(feature => {
|
||||
config.value.features[id] = feature;
|
||||
}).catch(() => {}).finally(() => {
|
||||
setTimeout(popup.close, 150);
|
||||
featureEditor.container.setAttribute('data-state', 'inactive');
|
||||
});
|
||||
|
||||
const popup = fullblocker([featureEditor.container], {
|
||||
priority: true, closeWhenOutside: false,
|
||||
});
|
||||
featureEditor.container.setAttribute('data-state', 'active');
|
||||
}
|
||||
function deleteFeature(id: string)
|
||||
{
|
||||
confirm("Voulez vous vraiment supprimer cet effet ?").then(e => {
|
||||
if(e)
|
||||
onMounted(() => {
|
||||
queueMicrotask(() => {
|
||||
if(container.value)
|
||||
{
|
||||
const value = config.value;
|
||||
delete value.features[id];
|
||||
config.value = value;
|
||||
const builder = new HomebrewBuilder(container.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Edition de données</Title>
|
||||
</Head>
|
||||
<TabsRoot class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full" default-value="features">
|
||||
<div ref="container" class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full"></div>
|
||||
<!-- <TabsRoot class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full" default-value="features">
|
||||
<TabsList class="flex flex-row gap-4 self-center relative px-4">
|
||||
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
|
||||
<TabsTrigger value="peoples" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples ({{ config.peoples.length }})</TabsTrigger>
|
||||
|
|
@ -79,7 +40,9 @@ function deleteFeature(id: string)
|
|||
<div class=""></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="training" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class=""></div>
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="abilities" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class=""></div>
|
||||
|
|
@ -109,5 +72,5 @@ function deleteFeature(id: string)
|
|||
</TabsContent>
|
||||
</div>
|
||||
</div>
|
||||
</TabsRoot>
|
||||
</TabsRoot> -->
|
||||
</template>
|
||||
|
|
@ -74,8 +74,8 @@
|
|||
},
|
||||
"arcana": {
|
||||
"max": [
|
||||
"intelligence",
|
||||
"psyche"
|
||||
"psyche",
|
||||
"intelligence"
|
||||
],
|
||||
"name": "Arcanes",
|
||||
"description": "La capacité à comprendre et percevoir la magie. Permet de comprendre un sort en cours, de détecter de la magie."
|
||||
|
|
@ -179,6 +179,14 @@
|
|||
"statistic": "psyche"
|
||||
}
|
||||
},
|
||||
"lists": {
|
||||
"sickness": [
|
||||
{
|
||||
"id": "",
|
||||
"name": "Pourriture mortelle"
|
||||
}
|
||||
]
|
||||
},
|
||||
"peoples": [
|
||||
{
|
||||
"name": "Humain",
|
||||
|
|
@ -2747,6 +2755,62 @@
|
|||
"operation": "add",
|
||||
"property": "health",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"id": "va1nyks173dvyraq6jmw6p41v7vvh3sr",
|
||||
"category": "choice",
|
||||
"text": "+1 aux jet de résistance de ",
|
||||
"options": [
|
||||
{
|
||||
"text": "Force",
|
||||
"category": "value",
|
||||
"operation": "add",
|
||||
"value": 1,
|
||||
"property": "modifier/strength"
|
||||
},
|
||||
{
|
||||
"text": "Dextérité",
|
||||
"category": "value",
|
||||
"operation": "add",
|
||||
"value": 1,
|
||||
"property": "modifier/dexterity"
|
||||
},
|
||||
{
|
||||
"text": "Constitution",
|
||||
"category": "value",
|
||||
"operation": "add",
|
||||
"value": 1,
|
||||
"property": "modifier/constitution"
|
||||
},
|
||||
{
|
||||
"text": "Intelligence",
|
||||
"category": "value",
|
||||
"operation": "add",
|
||||
"value": 1,
|
||||
"property": "modifier/intelligence"
|
||||
},
|
||||
{
|
||||
"text": "Curiosité",
|
||||
"category": "value",
|
||||
"operation": "add",
|
||||
"value": 1,
|
||||
"property": "modifier/curiosity"
|
||||
},
|
||||
{
|
||||
"text": "Charisme",
|
||||
"category": "value",
|
||||
"operation": "add",
|
||||
"value": 1,
|
||||
"property": "modifier/charisma"
|
||||
},
|
||||
{
|
||||
"text": "Psyché",
|
||||
"category": "value",
|
||||
"operation": "add",
|
||||
"value": 1,
|
||||
"property": "modifier/psyche"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -4664,6 +4728,7 @@
|
|||
"effect": [
|
||||
{
|
||||
"category": "choice",
|
||||
"text": "+1 au mod. de ",
|
||||
"id": "p3omttdrld3bj1mota2pi2fvt6kqe07n",
|
||||
"options": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureID, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import { z } from "zod/v4";
|
||||
import characterConfig from './character-config.json';
|
||||
import { button, fakeA, input, loading } from "./proses";
|
||||
import { div, dom, icon, text } from "./dom.util";
|
||||
import { popper } from "./floating.util";
|
||||
import { button, fakeA, input, loading, numberpicker, select } from "./proses";
|
||||
import { div, dom, icon, mergeClasses, text, type Class } from "./dom.util";
|
||||
import { contextmenu, followermenu, popper } from "./floating.util";
|
||||
import { clamp } from "./general.util";
|
||||
import markdownUtil from "./markdown.util";
|
||||
|
||||
|
|
@ -285,7 +285,7 @@ export class CharacterBuilder
|
|||
new AspectPicker(this),
|
||||
];
|
||||
this._helperText = text("Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.")
|
||||
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
|
||||
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
|
||||
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._stepsHeader), div(undefined, [ popper(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), {
|
||||
|
|
@ -381,9 +381,15 @@ export class CharacterBuilder
|
|||
let sum = 0;
|
||||
for(let i = 0; i < buffer.list.length; i++)
|
||||
{
|
||||
if(typeof buffer.list[i]!.value === 'string')
|
||||
if(typeof buffer.list[i]!.value === 'string') // Add or set a modifier
|
||||
{
|
||||
if(this._buffer[buffer.list[i]!.value as string]!._dirty)
|
||||
const modifier = this._buffer[buffer.list[i]!.value as string];
|
||||
if(!modifier)
|
||||
{
|
||||
queue.push(property);
|
||||
return;
|
||||
}
|
||||
else if(modifier._dirty)
|
||||
{
|
||||
//Put it back in queue since its dependencies haven't been resolved yet
|
||||
queue.push(property);
|
||||
|
|
@ -392,9 +398,9 @@ export class CharacterBuilder
|
|||
else
|
||||
{
|
||||
if(buffer.list[i]?.operation === 'add')
|
||||
sum += this._buffer[buffer.list[i]!.value as string]!.value;
|
||||
sum += modifier.value;
|
||||
else if(buffer.list[i]?.operation === 'set')
|
||||
sum = this._buffer[buffer.list[i]!.value as string]!.value;
|
||||
sum = modifier.value;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -415,7 +421,7 @@ export class CharacterBuilder
|
|||
this._buffer[property]!.value = sum;
|
||||
this._buffer[property]!._dirty = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
updateLevel(level: Level)
|
||||
{
|
||||
|
|
@ -609,6 +615,61 @@ export class CharacterBuilder
|
|||
}
|
||||
}
|
||||
|
||||
type PickableFeatureSettings = { state?: boolean, onToggle?: (state: boolean) => void, onChoice?: (options: number[]) => void, disabled?: boolean, class?: { selected?: Class, container?: Class, disabled?: Class }, choices?: Record<string, number[]>, };
|
||||
export class PickableFeature
|
||||
{
|
||||
private _content: HTMLElement;
|
||||
|
||||
private _feature: Feature;
|
||||
|
||||
private _characterChoices?: Record<string, number[]>;
|
||||
private _choiceDom?: HTMLElement;
|
||||
private _choices?: Extract<FeatureItem, { category: 'choice' }>[];
|
||||
|
||||
private _settings?: PickableFeatureSettings;
|
||||
|
||||
constructor(feature: FeatureID, settings?: PickableFeatureSettings)
|
||||
{
|
||||
this._feature = config.features[feature]!;
|
||||
this._settings = settings;
|
||||
|
||||
if(settings?.choices)
|
||||
{
|
||||
this._characterChoices = settings.choices;
|
||||
this._choices = this._feature.effect.filter(e => e.category === 'choice');
|
||||
this._choiceDom = this._choices.length > 0 ? dom('div', { class: 'absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 group-data-[active]:hover:border-light-70 dark:group-data-[active]:hover:border-dark-70 flex p-1 justify-center items-center', listeners: { click: (e) => e.stopImmediatePropagation() ?? this.choose() } }, [ icon('radix-icons:gear') ]) : undefined;
|
||||
}
|
||||
|
||||
this._content = dom("div", { attributes: { 'data-active': settings?.state, 'data-disabled': settings?.disabled ?? false }, class: ["group border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-70 dark:hover:border-dark-70 relative data-[active]:!border-accent-blue data-[active]:bg-accent-blue data-[active]:bg-opacity-20 data-[disabled]:hover:border-light-40 dark:data-[disabled]:hover:border-dark-40 data-[disabled]:opacity-30 data-[disabled]:cursor-default", settings?.class?.container, settings?.class?.selected ? mergeClasses(settings?.class?.selected).split(' ').map(e => `data-[state='active']:${e}`).join(' ') : undefined, settings?.class?.disabled ? mergeClasses(settings?.class?.disabled).split(' ').map(e => `data-[disabled]:${e}`).join(' ') : undefined], listeners: { click: e => this.toggle() }}, [
|
||||
markdownUtil(this._feature.description, undefined, { tags: { a: fakeA } }),
|
||||
this._choiceDom,
|
||||
]);
|
||||
}
|
||||
toggle(state?: boolean)
|
||||
{
|
||||
if(this._content.hasAttribute('data-disabled'))
|
||||
return this._content.hasAttribute('data-active');
|
||||
|
||||
const s = this._content.toggleAttribute('data-active', state);
|
||||
|
||||
this._settings?.onToggle && this._settings?.onToggle(s);
|
||||
|
||||
return s;
|
||||
}
|
||||
choose()
|
||||
{
|
||||
if(!this._choices || this._choices.length === 0)
|
||||
return;
|
||||
|
||||
const menu = followermenu(this._choiceDom!, [ div('px-24 py-6 flex flex-col items-center text-light-100 dark:text-dark-100', this._choices.map(e => div('flex flex-row items-center', [ text(e.text), div('flex flex-col', Array(e.settings?.amount ?? 1).fill(0).map((_, i) => (
|
||||
select(e.options.map((_e, _i) => ({ text: _e.text, value: _i })), { defaultValue: this._characterChoices![e.id] !== undefined ? this._characterChoices![e.id]![i] : undefined, change: (value) => { this._characterChoices![e.id] ??= []; this._characterChoices![e.id]![i] = value }, class: { container: 'w-32' } })
|
||||
))) ]))) ], { arrow: true, offset: { mainAxis: 8 }, cover: 'width', placement: 'bottom', priority: false, viewport: document.getElementById('characterEditorContainer') ?? undefined, });
|
||||
}
|
||||
get dom()
|
||||
{
|
||||
return this._content;
|
||||
}
|
||||
}
|
||||
interface BuilderTab {
|
||||
dom: Array<Node | string>;
|
||||
update: () => void;
|
||||
|
|
@ -702,42 +763,16 @@ class LevelPicker implements BuilderTab
|
|||
{
|
||||
this._builder = builder;
|
||||
|
||||
this._levelInput = dom("input", { 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`, listeners: {
|
||||
input: (e: Event) => {
|
||||
this._builder.character.level = parseInt(this._levelInput.value) ?? 1;
|
||||
this._levelInput = numberpicker({ defaultValue: this._builder.character.level, min: 1, max: 20, input: (value) => {
|
||||
this._builder.character.level = value;
|
||||
this.updateLevel();
|
||||
},
|
||||
keydown: (e: KeyboardEvent) => {
|
||||
let value = this._levelInput.value;
|
||||
switch(e.key)
|
||||
{
|
||||
case "ArrowUp":
|
||||
value = clamp(parseInt(value) + 1, 1, 20).toString();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
value = clamp(parseInt(value) - 1, 1, 20).toString();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(this._levelInput.value !== value)
|
||||
{
|
||||
this._levelInput.value = value;
|
||||
this._builder.character.level = parseInt(value);
|
||||
this.updateLevel();
|
||||
}
|
||||
}
|
||||
}});
|
||||
} });
|
||||
this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
|
||||
this._healthText = text("0"), this._manaText = text("0");
|
||||
|
||||
this._options = Object.entries(config.peoples[this._builder.character.people!]!.options).map(
|
||||
(level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative left-4" }, [ text(level[0]) ])]),
|
||||
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: 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 ])))
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => new PickableFeature(option, { disabled: parseInt(level[0], 10) > this._builder.character.level, state: this._builder.character.leveling[parseInt(level[0], 10) as Level] === j, choices: this._builder.character.choices }).dom))
|
||||
]);
|
||||
|
||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
|
||||
|
|
@ -776,14 +811,14 @@ class LevelPicker implements BuilderTab
|
|||
this._builder.updateLevel(this._builder.character.level as Level);
|
||||
|
||||
this._pointsInput.value = (this._builder.character.level - Object.keys(this._builder.character.leveling).length).toString();
|
||||
this._options.forEach((e, i) => {
|
||||
/* this._options.forEach((e, i) => {
|
||||
e[0]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
|
||||
e[1]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
|
||||
e[1]?.childNodes.forEach((option, j) => {
|
||||
'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level));
|
||||
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j));
|
||||
})
|
||||
});
|
||||
}); */
|
||||
}
|
||||
validate(): boolean
|
||||
{
|
||||
|
|
@ -813,10 +848,7 @@ class TrainingPicker implements BuilderTab
|
|||
const statRenderBlock = (stat: MainStat) => {
|
||||
return Object.entries(config.training[stat]).map(
|
||||
(level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
|
||||
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(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ])))
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => new PickableFeature(option, { state: level[0] == '0' || this._builder.character.training[stat as MainStat][level[0] as any as TrainingLevel] === j, choices: this._builder.character.choices }).dom))
|
||||
])
|
||||
}
|
||||
this._builder = builder;
|
||||
|
|
@ -869,17 +901,6 @@ class TrainingPicker implements BuilderTab
|
|||
this._pointsInput.value = ((values.training ?? 0) - training).toString();
|
||||
this._healthText.textContent = values.health?.toString() ?? '0';
|
||||
this._manaText.textContent = values.mana?.toString() ?? '0';
|
||||
|
||||
Object.keys(this._options).forEach(stat => {
|
||||
const max = Object.keys(this._builder.character.training[stat as MainStat]).length;
|
||||
this._options[stat as MainStat].forEach((e, i) => {
|
||||
e[0]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
|
||||
e[1]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
|
||||
e[1]?.childNodes.forEach((option, j) => {
|
||||
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j)));
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
validate(): boolean
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,13 +1,278 @@
|
|||
import type { Ability, Feature, FeatureEffect, FeatureItem, MainStat } from "~/types/character";
|
||||
import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat } from "~/types/character";
|
||||
import { div, dom, icon, text, type NodeChildren } from "./dom.util";
|
||||
import { MarkdownEditor } from "./editor.util";
|
||||
import { button, combobox, fakeA, numberpicker, select, type Option } from "./proses";
|
||||
import { tooltip } from "./floating.util";
|
||||
import { mainStatShortTexts, mainStatTexts } from "./character.util";
|
||||
import { button, combobox, fakeA, input, numberpicker, select, type Option } from "./proses";
|
||||
import { fullblocker, tooltip } from "./floating.util";
|
||||
import { MAIN_STATS, mainStatShortTexts, mainStatTexts } from "./character.util";
|
||||
import config from "#shared/character-config.json";
|
||||
import { getID, ID_SIZE } from "./general.util";
|
||||
import { clamp, getID, ID_SIZE } from "./general.util";
|
||||
import renderMarkdown from "./markdown.util";
|
||||
import { Tree } from "./tree";
|
||||
import markdownUtil from "./markdown.util";
|
||||
|
||||
const tabTexts: Record<string, string> = {
|
||||
|
||||
};
|
||||
export class HomebrewBuilder
|
||||
{
|
||||
private _container: HTMLDivElement;
|
||||
private _content?: HTMLDivElement;
|
||||
private _tabsHeader: HTMLDivElement[] = [];
|
||||
private _tabsContent: BuilderTab[] = [];
|
||||
|
||||
private _config: CharacterConfig;
|
||||
private _editor: FeatureEditor;
|
||||
|
||||
constructor(container: HTMLDivElement)
|
||||
{
|
||||
this._config = config as CharacterConfig;
|
||||
this._editor = new FeatureEditor();
|
||||
this._container = container;
|
||||
|
||||
this._tabsHeader = [
|
||||
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")]),
|
||||
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")]),
|
||||
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("Compétences")]),
|
||||
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("Aspect")]),
|
||||
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("Sorts")]),
|
||||
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(5) } }, [text("Listes")]),
|
||||
];
|
||||
this._tabsContent = [
|
||||
new PeopleEditor(this, this._config),
|
||||
new TrainingEditor(this, this._config),
|
||||
new AbilityEditor(this, this._config),
|
||||
new AspectEditor(this, this._config),
|
||||
/* new SpellEditor(this),
|
||||
new ListEditor(this), */
|
||||
];
|
||||
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
|
||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||
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._content,
|
||||
]));
|
||||
|
||||
this.display(1);
|
||||
}
|
||||
display(tab: number)
|
||||
{
|
||||
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)
|
||||
{
|
||||
const promise = this._editor.edit(feature).then(f => {
|
||||
this._config.features[feature.id] = f;
|
||||
}).finally(() => {
|
||||
setTimeout(popup.close, 150);
|
||||
this._editor.container.setAttribute('data-state', 'inactive');
|
||||
});
|
||||
const popup = fullblocker([this._editor.container], {
|
||||
priority: true, closeWhenOutside: false,
|
||||
});
|
||||
this._editor.container.setAttribute('data-state', 'active');
|
||||
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
|
||||
{
|
||||
private _options: HTMLDivElement[];
|
||||
|
||||
private _activeOption?: HTMLDivElement;
|
||||
|
||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
{
|
||||
super(builder, config);
|
||||
|
||||
this._options = config.peoples.map(
|
||||
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, false));
|
||||
this._activeOption = this._options[i]!;
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._activeOption?.classList.toggle(e, true));
|
||||
}
|
||||
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
|
||||
);
|
||||
|
||||
this._content = [ div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options) ];
|
||||
}
|
||||
}
|
||||
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);
|
||||
const statRenderBlock = (stat: MainStat) => {
|
||||
return Object.entries(config.training[stat]).map(
|
||||
(level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
|
||||
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.edit(config.features[option]!);
|
||||
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ])))
|
||||
])
|
||||
}
|
||||
|
||||
this._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' });
|
||||
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]))));
|
||||
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", [
|
||||
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' })),
|
||||
this._statIndicator,
|
||||
]),
|
||||
]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])];
|
||||
|
||||
this.switchTab(0);
|
||||
}
|
||||
switchTab(tab: number)
|
||||
{
|
||||
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 AbilityEditor extends BuilderTab
|
||||
{
|
||||
private _options: HTMLDivElement[];
|
||||
|
||||
private _tooltips: Text[] = [];
|
||||
private _maxs: HTMLElement[] = [];
|
||||
|
||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
{
|
||||
super(builder, config);
|
||||
|
||||
const numberInput = (value?: number, update?: (value: number) => number | undefined) => {
|
||||
const input = dom("input", { 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`, listeners: {
|
||||
input: (e: Event) => {
|
||||
input.value = (update && update(parseInt(input.value))?.toString()) ?? input.value;
|
||||
},
|
||||
keydown: (e: KeyboardEvent) => {
|
||||
let value = isNaN(parseInt(input.value)) ? '0' : input.value;
|
||||
switch(e.key)
|
||||
{
|
||||
case "ArrowUp":
|
||||
value = clamp(parseInt(value) + 1, 0, 99).toString();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
value = clamp(parseInt(value) - 1, 0, 99).toString();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(input.value !== value)
|
||||
{
|
||||
input.value = (update && update(parseInt(value))?.toString()) ?? value;
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
input.value = value?.toString() ?? "0";
|
||||
return input;
|
||||
};
|
||||
function pushAndReturn<T extends any>(arr: Array<T>, value: T): T
|
||||
{
|
||||
arr.push(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/* this._options = ABILITIES.map((e, i) => div('flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative', [
|
||||
div('flex justify-between', [ numberpicker({ defaultValue: this._builder.character.abilities[e], input: (value) => {
|
||||
const values = this._builder.values;
|
||||
const max = (values[`abilities/${e}/max`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0);
|
||||
|
||||
this._builder.character.abilities[e] = clamp(value, 0, max);
|
||||
Object.assign((this._options[i]?.lastElementChild as HTMLSpanElement | undefined)?.style ?? {}, { width: `${(max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100}%` });
|
||||
this._tooltips[i]!.textContent = `${mainStatTexts[config.abilities[e].max[0]]} (${values[`modifier/${config.abilities[e].max[0]}`] ?? 0}) + ${mainStatTexts[config.abilities[e].max[1]]} (${values[`modifier/${config.abilities[e].max[1]}`] ?? 0}) + ${values[`abilities/${e}/max`] ?? 0}`;
|
||||
this._maxs[i]!.textContent = `/ ${max ?? 0}`;
|
||||
|
||||
const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0);
|
||||
this._pointsInput.value = ((values.ability ?? 0) - abilities).toString();
|
||||
|
||||
return this._builder.character.abilities[e];
|
||||
}}), popper(pushAndReturn(this._maxs, dom('span', { class: 'text-lg text-end cursor-pointer', text: '' })), {
|
||||
arrow: true,
|
||||
offset: 6,
|
||||
placement: 'bottom-end',
|
||||
class: 'max-w-96 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',
|
||||
content: [ pushAndReturn(this._tooltips, text('')) ]
|
||||
})]),
|
||||
dom('span', { class: "text-xl text-center font-bold", text: config.abilities[e].name }),
|
||||
dom('span', { class: "absolute -bottom-px -left-px h-[3px] bg-accent-blue" }),
|
||||
])); */
|
||||
|
||||
this._content = [ div('flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48', /* this._options */)];
|
||||
}
|
||||
}
|
||||
class AspectEditor extends BuilderTab
|
||||
{
|
||||
private _filter: boolean = true;
|
||||
|
||||
private _options: HTMLDivElement[];
|
||||
|
||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
{
|
||||
super(builder, config);
|
||||
|
||||
/* this._options = config.aspects.map((e, i) => dom('div', { attributes: { "data-aspect": i.toString() }, listeners: { click: () => {
|
||||
this._builder.character.aspect = i;
|
||||
this._options.forEach(_e => _e.setAttribute('data-state', 'inactive'));
|
||||
this._options[i]?.setAttribute('data-state', 'active');
|
||||
}}, class: 'group flex flex-col w-[360px] border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 cursor-pointer' }, [
|
||||
div('bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2 group-data-[state=active]:bg-accent-blue group-data-[state=active]:bg-opacity-10', [
|
||||
div('flex flex-row gap-8 ps-4 items-center', [
|
||||
div("flex flex-1 flex-col gap-2 justify-center", [ div('text-lg font-bold', [ text(e.name) ]), dom('span', { class: 'border-b w-full border-light-50 dark:border-dark-50 group-data-[state=active]:border-b-[4px] group-data-[state=active]:border-accent-blue' }) ]),
|
||||
div('rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10')
|
||||
])
|
||||
]),
|
||||
div('flex justify-stretch items-stretch py-2 px-4 gap-4', [
|
||||
div('flex flex-col flex-1 items-stretch gap-4', [
|
||||
div('flex flex-1 justify-between', [ text('Difficulté'), div('text-sm font-bold', [ text(e.difficulty.toString()) ]) ]),
|
||||
div('flex flex-1 justify-between', [ text('Bonus'), div('text-sm font-bold', [ text(e.stat === 'special' ? 'Special' : mainStatTexts[e.stat]) ]) ])
|
||||
]),
|
||||
div('w-px h-full bg-light-50 dark:bg-dark-50'),
|
||||
div('flex flex-col items-center justify-between py-2', [
|
||||
div('text-sm italic', [ text(alignmentToString(e.alignment)) ]),
|
||||
div(['text-sm font-bold', { "text-light-purple dark:text-dark-purple italic": e.magic, "text-light-orange dark:text-dark-orange": !e.magic }], [ text(e.magic ? 'Magie autorisée' : 'Magie interdite') ]),
|
||||
]),
|
||||
])
|
||||
])); */
|
||||
|
||||
this._content = [ div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', /* this._options */)];
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureEditor
|
||||
{
|
||||
|
|
@ -79,10 +344,10 @@ export class FeatureEditor
|
|||
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'), () => {
|
||||
}, 'p-2 -m-px hover:z-10 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") ])
|
||||
}, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Supprimer", "bottom") ])
|
||||
]) ]);
|
||||
return content;
|
||||
}
|
||||
|
|
@ -93,8 +358,8 @@ export class FeatureEditor
|
|||
{
|
||||
case 'value':
|
||||
return flattenFeatureChoices.find(e => e.category === 'value' && e.property === effect.property);
|
||||
/* case 'choice':
|
||||
return choices.find(e => e.value.category === 'choice' && e.value. === effect.property); */
|
||||
case 'choice':
|
||||
return flattenFeatureChoices.find(e => e.category === 'choice');
|
||||
case 'list':
|
||||
return flattenFeatureChoices.find(e => e.category === 'list' && e.list === effect.list);
|
||||
}
|
||||
|
|
@ -125,15 +390,15 @@ export class FeatureEditor
|
|||
case 'value':
|
||||
const summaryText = text(textFromEffect(buffer));
|
||||
top = [
|
||||
select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property) || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { 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] w-[80px]' } }),
|
||||
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] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } }),
|
||||
select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property) || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { 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 hover:z-10 h-[36px] w-[80px]' } }),
|
||||
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 hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as 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'),
|
||||
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'),
|
||||
];
|
||||
bottom = [div('px-2 py-1', [summaryText])];
|
||||
break;
|
||||
|
|
@ -142,7 +407,7 @@ export class FeatureEditor
|
|||
{
|
||||
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]' } }) ];
|
||||
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 hover:z-10 h-[36px]' } }) ];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -153,13 +418,25 @@ export class FeatureEditor
|
|||
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-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' } }) ];
|
||||
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 hover:z-10 h-[36px] w-32' } }) ];
|
||||
break;
|
||||
case 'choice':
|
||||
const add = () => {
|
||||
const option: Extract<FeatureItem, { category: 'choice' }>["options"][number] = { id: getID(ID_SIZE), category: 'value', text: '', operation: 'add', property: '', value: 0 };
|
||||
(buffer as Extract<FeatureItem, { category: 'choice' }>).options.push(option);
|
||||
render(option);
|
||||
};
|
||||
const render = (option: FeatureEffect) => {
|
||||
|
||||
}
|
||||
const list = div('flex flex-row');
|
||||
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' }) ];
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return div('border border-light-30 dark:border-dark-30 col-span-2 row-span-2', [ div('flex justify-between items-stretch', [
|
||||
div('flex flex-row', [
|
||||
combobox(featureChoices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px h-[36px]' }, change: (e) => {
|
||||
div('flex flex-row flex-1', [
|
||||
combobox(featureChoices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, change: (e) => {
|
||||
buffer = { id: buffer.id, ...e } as FeatureItem;
|
||||
const element = redraw();
|
||||
this._table.replaceChild(element, content);
|
||||
|
|
@ -167,7 +444,7 @@ export class FeatureEditor
|
|||
} }),
|
||||
...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', [ tooltip(button(icon('radix-icons:check'), approve, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), reject, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ])
|
||||
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-auto items-center', bottom) ]);
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +501,7 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
|
|||
{ 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: [] }, },
|
||||
{ text: 'Choix', value: { category: 'choice', text: '', options: [] }, },
|
||||
];
|
||||
const flattenFeatureChoices = Tree.accumulate(featureChoices, 'value', (item) => Array.isArray(item.value) ? undefined : item.value).filter(e => !!e) as Partial<FeatureItem>[];
|
||||
function textFromEffect(effect: FeatureItem)
|
||||
|
|
@ -320,12 +597,14 @@ function textFromEffect(effect: FeatureItem)
|
|||
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'}".`;
|
||||
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':
|
||||
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'}".`;
|
||||
}
|
||||
}
|
||||
else if(effect.category === 'choice')
|
||||
{
|
||||
return `Choix (WIP)`;
|
||||
return `${effect.text} (${effect.options.length} options)`;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@ import * as FloatingUI from "@floating-ui/dom";
|
|||
import { cancelPropagation, dom, svg, text, type Class, type NodeChildren } from "./dom.util";
|
||||
import { button } from "./proses";
|
||||
|
||||
export interface ContextProperties
|
||||
export interface FloatingProperties
|
||||
{
|
||||
placement?: FloatingUI.Placement;
|
||||
offset?: number;
|
||||
offset?: FloatingUI.OffsetOptions;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
style?: Record<string, string | undefined | boolean | number> | string;
|
||||
viewport?: HTMLElement;
|
||||
cover?: 'width' | 'height' | 'all' | 'none';
|
||||
}
|
||||
export interface PopperProperties extends ContextProperties
|
||||
export interface FollowerProperties extends FloatingProperties
|
||||
{
|
||||
blur?: () => void;
|
||||
priority?: boolean;
|
||||
}
|
||||
export interface PopperProperties extends FloatingProperties
|
||||
{
|
||||
content?: NodeChildren;
|
||||
delay?: number;
|
||||
|
|
@ -36,7 +42,7 @@ export function init()
|
|||
export function popper(container: HTMLElement, properties?: PopperProperties): HTMLElement
|
||||
{
|
||||
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 arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
|
||||
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';
|
||||
|
||||
|
|
@ -47,24 +53,32 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||
strategy: 'fixed',
|
||||
middleware: [
|
||||
properties?.offset ? FloatingUI.offset(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, strategy: "escaped" }),
|
||||
FloatingUI.hide({ rootBoundary: rect }),
|
||||
FloatingUI.shift({ rootBoundary: rect }),
|
||||
FloatingUI.flip({ rootBoundary: rect }),
|
||||
properties?.cover && properties?.cover !== 'none' && FloatingUI.size({ rootBoundary: rect, apply: ({ availableWidth, availableHeight }) => {
|
||||
if(properties?.cover === 'width' || properties?.cover === 'all')
|
||||
content.style.width = `${availableWidth}px`;
|
||||
if(properties?.cover === 'height' || properties?.cover === 'all')
|
||||
content.style.height = `${availableHeight}px`;
|
||||
} }),
|
||||
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
|
||||
]
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(content.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
visibility: middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped ? 'hidden' : 'visible',
|
||||
});
|
||||
|
||||
const side = placement.split('-')[0] as FloatingUI.Side;
|
||||
|
||||
content.setAttribute('data-side', side);
|
||||
|
||||
if(properties?.offset && properties?.arrow)
|
||||
if(middlewareData.arrow)
|
||||
{
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow!;
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow;
|
||||
|
||||
const staticSide = {
|
||||
top: 'bottom',
|
||||
|
|
@ -85,7 +99,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||
top: arrowY != null ? `${arrowY}px` : '',
|
||||
right: '',
|
||||
bottom: '',
|
||||
[staticSide]: `-6px`,
|
||||
[staticSide]: `-8px`,
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
});
|
||||
}
|
||||
|
|
@ -152,52 +166,46 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||
|
||||
return container;
|
||||
}
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: ContextProperties & { blur?: () => void })
|
||||
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
|
||||
{
|
||||
const virtual = {
|
||||
getBoundingClientRect() {
|
||||
return {
|
||||
x: x,
|
||||
y: y,
|
||||
top: y,
|
||||
left: x,
|
||||
bottom: y,
|
||||
right: x,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
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 arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,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], style: properties?.style }, content);
|
||||
|
||||
function update()
|
||||
{
|
||||
FloatingUI.computePosition(virtual, container, {
|
||||
FloatingUI.computePosition(target, container, {
|
||||
placement: properties?.placement,
|
||||
strategy: 'fixed',
|
||||
middleware: [
|
||||
properties?.offset ? FloatingUI.offset(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, strategy: "escaped" }),
|
||||
FloatingUI.hide({ rootBoundary: rect }),
|
||||
FloatingUI.shift({ rootBoundary: rect }),
|
||||
FloatingUI.flip({ rootBoundary: rect }),
|
||||
properties?.cover && properties?.cover !== 'none' && FloatingUI.size({ rootBoundary: rect, apply: ({ availableWidth, availableHeight }) => {
|
||||
if(properties?.cover === 'width' || properties?.cover === 'all')
|
||||
container.style.width = `${availableWidth}px`;
|
||||
if(properties?.cover === 'height' || properties?.cover === 'all')
|
||||
container.style.height = `${availableHeight}px`;
|
||||
} }),
|
||||
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
|
||||
]
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(container.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
visibility: middlewareData.hide?.referenceHidden || middlewareData.hide?.escaped ? 'hidden' : 'visible',
|
||||
});
|
||||
|
||||
const side = placement.split('-')[0] as FloatingUI.Side;
|
||||
|
||||
container.setAttribute('data-side', side);
|
||||
|
||||
if(properties?.offset && properties?.arrow)
|
||||
if(middlewareData.arrow)
|
||||
{
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow!;
|
||||
const { x: arrowX, y: arrowY } = middlewareData.arrow;
|
||||
|
||||
const staticSide = {
|
||||
top: 'bottom',
|
||||
|
|
@ -218,7 +226,7 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
|||
top: arrowY != null ? `${arrowY}px` : '',
|
||||
right: '',
|
||||
bottom: '',
|
||||
[staticSide]: `-6px`,
|
||||
[staticSide]: `-8px`,
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
});
|
||||
}
|
||||
|
|
@ -226,10 +234,10 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
|||
}
|
||||
update();
|
||||
|
||||
document.addEventListener('mousedown', close);
|
||||
properties?.priority || document.addEventListener('mousedown', close);
|
||||
container.addEventListener('mousedown', cancelPropagation);
|
||||
|
||||
const stop = FloatingUI.autoUpdate(virtual, container, update, {
|
||||
const stop = FloatingUI.autoUpdate(target, container, update, {
|
||||
animationFrame: true,
|
||||
layoutShift: false,
|
||||
elementResize: false,
|
||||
|
|
@ -240,7 +248,7 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
|||
|
||||
function close()
|
||||
{
|
||||
document.removeEventListener('mousedown', close);
|
||||
properties?.priority || document.removeEventListener('mousedown', close);
|
||||
container.removeEventListener('mousedown', cancelPropagation);
|
||||
|
||||
container.remove();
|
||||
|
|
@ -250,6 +258,23 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
|||
|
||||
return { close, container, content };
|
||||
}
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: FollowerProperties)
|
||||
{
|
||||
return followermenu({
|
||||
getBoundingClientRect() {
|
||||
return {
|
||||
x: x,
|
||||
y: y,
|
||||
top: y,
|
||||
left: x,
|
||||
bottom: y,
|
||||
right: x,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
},
|
||||
}, content, properties);
|
||||
}
|
||||
export function tooltip(container: HTMLElement, txt: string, placement: FloatingUI.Placement, delay?: number): HTMLElement
|
||||
{
|
||||
return popper(container, {
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ export const a: Prose = {
|
|||
arrow: true,
|
||||
delay: 150,
|
||||
offset: 12,
|
||||
cover: "height",
|
||||
placement: 'bottom-start',
|
||||
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full z-[45]',
|
||||
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto z-[45]',
|
||||
content: [loading("large")],
|
||||
viewport: document.getElementById('mainContainer') ?? undefined,
|
||||
onShow(content: HTMLDivElement) {
|
||||
|
|
@ -50,7 +51,7 @@ export const a: Prose = {
|
|||
Content.getContent(overview.id).then((_content) => {
|
||||
if(_content?.type === 'markdown')
|
||||
{
|
||||
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' }), content.children[0]!);
|
||||
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'py-4 px-6' }), content.children[0]!);
|
||||
}
|
||||
if(_content?.type === 'canvas')
|
||||
{
|
||||
|
|
@ -90,6 +91,7 @@ export const fakeA: Prose = {
|
|||
arrow: true,
|
||||
delay: 150,
|
||||
offset: 12,
|
||||
cover: "height",
|
||||
placement: 'bottom-start',
|
||||
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full z-[45]',
|
||||
content: [loading("large")],
|
||||
|
|
@ -360,9 +362,9 @@ export function combobox<T extends NonNullable<any>>(options: Option<T>[], setti
|
|||
})
|
||||
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
|
||||
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, placeholder?: string }): 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
|
||||
const input = dom("input", { attributes: { disabled: settings?.disabled, placeholder: settings?.placeholder }, 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),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export type SpellType = typeof SPELL_TYPES[number];
|
|||
export type Category = typeof CATEGORIES[number];
|
||||
export type SpellElement = typeof SPELL_ELEMENTS[number];
|
||||
|
||||
export type DoubleIndex<T extends number | string> = [T, number];
|
||||
export type FeatureID = string;
|
||||
export type Alignment = { loyalty: 'loyal' | 'neutral' | 'chaotic', kindness: 'good' | 'neutral' | 'evil' };
|
||||
|
||||
export type Character = {
|
||||
|
|
@ -34,17 +34,21 @@ export type Character = {
|
|||
username?: string;
|
||||
visibility: "private" | "public";
|
||||
};
|
||||
export type CharacterValues = {
|
||||
export type CharacterVariables = {
|
||||
health: number;
|
||||
mana: number;
|
||||
|
||||
sickness: Array<{ id: string, progress: number | true }>;
|
||||
equipment: Array<string>;
|
||||
};
|
||||
export type CharacterConfig = {
|
||||
peoples: RaceConfig[],
|
||||
training: Record<MainStat, Record<TrainingLevel, string[]>>;
|
||||
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
|
||||
abilities: Record<Ability, AbilityConfig>;
|
||||
spells: SpellConfig[];
|
||||
aspects: AspectConfig[];
|
||||
features: Record<string, Feature>;
|
||||
features: Record<FeatureID, Feature>;
|
||||
lists: Record<string, { id: string, name: string, [key: string]: any }[]>;
|
||||
};
|
||||
export type SpellConfig = {
|
||||
id: string;
|
||||
|
|
@ -65,7 +69,7 @@ export type AbilityConfig = {
|
|||
export type RaceConfig = {
|
||||
name: string;
|
||||
description: string;
|
||||
options: Record<Level, string[]>;
|
||||
options: Record<Level, FeatureID[]>;
|
||||
};
|
||||
export type AspectConfig = {
|
||||
name: string;
|
||||
|
|
@ -81,22 +85,23 @@ export type AspectConfig = {
|
|||
};
|
||||
|
||||
export type FeatureEffect = {
|
||||
id: string;
|
||||
id: FeatureID;
|
||||
category: "value";
|
||||
operation: "add" | "set";
|
||||
property: string;
|
||||
value: number | `modifier/${MainStat}` | false;
|
||||
} | {
|
||||
id: string;
|
||||
id: FeatureID;
|
||||
category: "list";
|
||||
list: "spells" | "action" | "reaction" | "freeaction" | "passive";
|
||||
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
|
||||
action: "add" | "remove";
|
||||
item: string;
|
||||
extra?: any;
|
||||
};
|
||||
export type FeatureItem = FeatureEffect | {
|
||||
id: string;
|
||||
id: FeatureID;
|
||||
category: "choice";
|
||||
text: string;
|
||||
settings?: { //If undefined, amount is 1 by default
|
||||
amount: number;
|
||||
exclusive: boolean; //Disallow to pick the same option twice
|
||||
|
|
@ -104,7 +109,7 @@ export type FeatureItem = FeatureEffect | {
|
|||
options: Array<FeatureEffect & { text: string }>;
|
||||
}
|
||||
export type Feature = {
|
||||
id: string;
|
||||
id: FeatureID;
|
||||
description: string;
|
||||
effect: FeatureItem[];
|
||||
};
|
||||
|
|
@ -125,7 +130,7 @@ export type CompiledCharacter = {
|
|||
capacity: number | false;
|
||||
initiative: number;
|
||||
|
||||
values: CharacterValues,
|
||||
variables: CharacterVariables,
|
||||
|
||||
defense: {
|
||||
hardcap: number;
|
||||
|
|
|
|||
Loading…
Reference in New Issue