diff --git a/db.sqlite b/db.sqlite index 1d76f59..decab8c 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-shm b/db.sqlite-shm index fe9ac28..c92b1c6 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index e69de29..fcf177c 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/pages/character/manage.client.vue b/pages/character/manage.client.vue index 43152fc..2b4f797 100644 --- a/pages/character/manage.client.vue +++ b/pages/character/manage.client.vue @@ -1,68 +1,29 @@ \ No newline at end of file diff --git a/shared/character-config.json b/shared/character-config.json index bbf4320..694df20 100644 --- a/shared/character-config.json +++ b/shared/character-config.json @@ -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": [ { diff --git a/shared/character.util.ts b/shared/character.util.ts index 292dcfc..9b2b41e 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -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, }; +export class PickableFeature +{ + private _content: HTMLElement; + + private _feature: Feature; + + private _characterChoices?: Record; + private _choiceDom?: HTMLElement; + private _choices?: Extract[]; + + 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; 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.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._levelInput = numberpicker({ defaultValue: this._builder.character.level, min: 1, max: 20, input: (value) => { + this._builder.character.level = 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 { diff --git a/shared/feature.util.ts b/shared/feature.util.ts index 2aba239..e9f06d8 100644 --- a/shared/feature.util.ts +++ b/shared/feature.util.ts @@ -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 = { + +}; +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; + + 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; + + 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); + + 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(arr: Array, 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).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract).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).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).value = value; summaryText.textContent = textFromEffect(buffer); } }), + select([ { text: '+', value: 'add' }, (['speed', 'capacity'].includes(buffer.property) || ['defense/'].some(e => (buffer as Extract).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract).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).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).value = value; summaryText.textContent = textFromEffect(buffer); } }), button(icon('radix-icons:update'), () => { (buffer as Extract).value = (typeof (buffer as Extract).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).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).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).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).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["options"][number] = { id: getID(ID_SIZE), category: 'value', text: '', operation: 'add', property: '', value: 0 }; + (buffer as Extract).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).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>[] = [ { 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[]; 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 { diff --git a/shared/floating.util.ts b/shared/floating.util.ts index 8531c74..eb53080 100644 --- a/shared/floating.util.ts +++ b/shared/floating.util.ts @@ -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; 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, { diff --git a/shared/proses.ts b/shared/proses.ts index 19153d4..e04bb2d 100644 --- a/shared/proses.ts +++ b/shared/proses.ts @@ -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>(options: Option[], 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), diff --git a/types/character.d.ts b/types/character.d.ts index 01969d5..0e99e28 100644 --- a/types/character.d.ts +++ b/types/character.d.ts @@ -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, 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; }; export type CharacterConfig = { peoples: RaceConfig[], - training: Record>; + training: Record>; abilities: Record; spells: SpellConfig[]; aspects: AspectConfig[]; - features: Record; + features: Record; + lists: Record; }; export type SpellConfig = { id: string; @@ -65,7 +69,7 @@ export type AbilityConfig = { export type RaceConfig = { name: string; description: string; - options: Record; + options: Record; }; 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; } 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;