Changes tooltips reference, update character sheet UI, getID now embed the ID_SIZE, new ability max option in feature effect.

This commit is contained in:
Clément Pons 2025-08-29 17:46:08 +02:00
parent 042d4479ee
commit 17bc232602
19 changed files with 522 additions and 242 deletions

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -67,7 +67,7 @@ import { TreeDOM } from '#shared/tree';
import { Content, iconByType } from '#shared/content.util';
import { dom, icon, text } from '#shared/dom.util';
import { unifySlug } from '#shared/general.util';
import { popper } from '#shared/floating.util';
import { popper, tooltip } from '#shared/floating.util';
import { link } from '#shared/components.util';
const options = ref<DropdownOption[]>([{
@ -94,13 +94,13 @@ const tree = new TreeDOM((item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: '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' }) : undefined,
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
])]);
}, (item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link({ class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined, [
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: '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' }) : undefined,
item.private ? tooltip(icon('radix-icons:lock-closed', { class: 'mx-1' }), 'Privé', 'right') : undefined,
])]);
}, (item) => item.navigable);
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));

View File

@ -84,10 +84,16 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
</div>
</div>
<div class="flex flex-1 px-8">
<div class="flex flex-col pe-8 gap-4 py-8 w-80 border-r border-light-30 dark:border-dark-30">
<div class="flex flex-col pe-8 gap-4 py-2 w-80 border-r border-light-30 dark:border-dark-30">
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'arme</span>
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30 mb-2">Compétences</span>
<div class="grid grid-cols-3 gap-1">
<div class="flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70" v-for="(value, ability) of character.abilities"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value }}</span><span>{{ characterConfig.abilities[ability].name }}</span></div>
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrises</span>
<div class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes légères" label="Arme légère" />
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes de jet" label="Arme de jet" />
<PreviewA v-if="character.mastery.strength + character.mastery.dexterity > 0" href="regles/annexes/equipement#Les armes naturelles" label="Arme naturelle" />
@ -101,52 +107,46 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
<PreviewA v-if="character.mastery.shield > 0" href="regles/annexes/equipement#Les boucliers" label="Bouclier" />
<PreviewA v-if="character.mastery.shield > 0 && character.mastery.strength > 3" href="regles/annexes/equipement#Les boucliers à deux mains" label="Bouclier à deux mains" />
</div>
</div>
<div v-if="character.mastery.armor > 0" class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'armure</span>
<div class="grid grid-cols-2 gap-x-3 gap-y-1">
<div v-if="character.mastery.armor > 0" class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
<PreviewA v-if="character.mastery.armor > 0" href="regles/annexes/equipement#Les armures légères" label="Armure légère" />
<PreviewA v-if="character.mastery.armor > 1" href="regles/annexes/equipement#Les armures" label="Armure standard" />
<PreviewA v-if="character.mastery.armor > 2" href="regles/annexes/equipement#Les armures lourdes" label="Armure lourde" />
</div>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise de sorts</span>
<span>Sorts de précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
<span>Sorts de savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
<span>Sorts d'instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30 mb-2">Compétences</span>
<div class="grid grid-cols-3 gap-1">
<div class="flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70" v-for="(value, ability) of character.abilities"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value }}</span><span>{{ characterConfig.abilities[ability].name }}</span></div>
<div class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
<span>Précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
<span>Savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
<span>Instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
<span>Oeuvres: <span class="font-bold">{{ character.spellranks.arts }}</span></span>
</div>
</div>
</div>
<TabsRoot default-value="features" class="w-[60rem]">
<TabsRoot default-value="features" class="w-[60rem] max-h-full">
<TabsList class="flex flex-row relative px-4 gap-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="features" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Aptitudes</TabsTrigger>
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue" v-if="character.spellslots > 0">Sorts</TabsTrigger>
<TabsTrigger value="inventory" class="px-2 py-1 border-b border-transparent hover:border-accent-blue" v-if="character.capacity !== false">Inventaire</TabsTrigger>
<TabsTrigger value="notes" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Notes</TabsTrigger>
</TabsList>
<TabsContent value="features">
<TabsContent value="features" class="overflow-y-auto max-h-full">
<div class="flex flex-1 flex-col ps-8 gap-4 py-4">
<div class="grid grid-cols-3 gap-4">
<div class="flex flex-col">
<div class="flex flex-col col-span-2">
<span class="text-lg font-semibold">Actions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
<MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Réactions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Actions libre</span>
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
<div class="flex flex-col gap-2">
<div class="flex flex-col">
<span class="text-lg font-semibold">Réactions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Actions libre</span>
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
</div>
</div>
</div>
<div class="flex flex-col">
@ -155,7 +155,7 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
</div>
</div>
</TabsContent>
<TabsContent v-if="character.spellslots > 0" value="spells">
<TabsContent v-if="character.spellslots > 0" value="spells" class="overflow-y-auto max-h-full">
<div class="flex flex-1 flex-col ps-8 gap-4 py-2">
<div class="flex flex-1 justify-between items-center"><span class="italic text-light-70 dark:text-dark-70 text-sm">{{ character.variables.spells.length }} / {{ character.spellslots }} sorts maitrisés</span><Button icon><Icon icon="radix-icons:plus" class="w-6 h-6"/></Button></div>
<div class="flex flex-col" v-if="[...(character.lists.spells ?? []), ...character.variables.spells].length > 0">
@ -179,7 +179,12 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
</div>
</div>
</TabsContent>
<TabsContent value="notes">
<TabsContent value="inventory" v-if="character.capacity !== false" class="overflow-y-auto max-h-full">
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
<MarkdownRenderer :content="character.notes" />
</div>
</TabsContent>
<TabsContent value="notes" class="overflow-y-auto max-h-full">
<div class="flex flex-1 flex-col ps-8 gap-4 py-8">
<MarkdownRenderer :content="character.notes" />
</div>

View File

@ -38,7 +38,7 @@
import { Content, Editor } from '#shared/content.util';
import { button, loading } from '#shared/components.util';
import { dom, icon, text } from '#shared/dom.util';
import { modal, popper } from '#shared/floating.util';
import { modal, popper, tooltip } from '#shared/floating.util';
definePageMeta({
rights: ['admin', 'editor'],
@ -79,8 +79,8 @@ onMounted(async () => {
tree.value.appendChild(load);
const content = dom('div', { class: 'flex flex-row justify-start items-center gap-4 p-2' }, [
popper(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Actualiser')], class: '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' }),
popper(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), { placement: 'top', offset: 4, delay: 120, arrow: true, content: [text('Enregistrer')], class: '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' }),
tooltip(button(icon('ph:cloud-arrow-down', { height: 20, width: 20 }), pull, 'p-1'), 'Actualiser', 'top'),
tooltip(button(icon('ph:cloud-arrow-up', { height: 20, width: 20 }), push, 'p-1'), 'Enregistrer', 'top'),
])
tree.value.insertBefore(content, load);

View File

@ -2,7 +2,7 @@ import useDatabase from "~/composables/useDatabase";
import { extname, basename } from 'node:path';
import type { CanvasColor, CanvasContent } from "~/types/canvas";
import type { FileType, ProjectContent } from "#shared/content.util";
import { getID, ID_SIZE, parsePath } from "#shared/general.util";
import { getID, parsePath } from "#shared/general.util";
import { projectContentTable, projectFilesTable } from "~/db/schema";
const typeMapping: Record<string, FileType> = {
@ -35,7 +35,7 @@ export default defineTask({
const title = basename(e.path);
const order = /(\d+)\. ?(.+)/gsmi.exec(title);
return {
id: getID(ID_SIZE),
id: getID(),
path: parsePath(e.path),
order: i,
title: title,
@ -54,7 +54,7 @@ export default defineTask({
const content = (await $fetch(`https://git.peaceultime.com/api/v1/repos/peaceultime/system-aspect/raw/${encodeURIComponent(e.path)}`));
return {
id: getID(ID_SIZE),
id: getID(),
path: parsePath(extension === '.md' ? e.path.replace(extension, '') : e.path),
order: i,
title: title,

View File

@ -2,7 +2,7 @@ import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
import { clamp, lerp } from "#shared/general.util";
import { dom, icon, svg, text } from "#shared/dom.util";
import render from "#shared/markdown.util";
import { popper } from "#shared/floating.util";
import { popper, tooltip } from "#shared/floating.util";
import { History } from "#shared/history.util";
import { fakeA } from "#shared/proses";
import { SpatialGrid } from "#shared/physics.util";
@ -412,15 +412,9 @@ export class Canvas
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none' }, [
dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom avant')], class: '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'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:corners')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Tout contenir')], class: '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'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom arrière')], class: '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'
}),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), 'Zoom avant', 'right'),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: (e: MouseEvent) => { this.reset(); } } }, [icon('radix-icons:corners')]), 'Tout contenir', 'right'),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), 'Zoom arrière', 'right'),
]),
]), this.transform,
]);
@ -707,31 +701,17 @@ export class CanvasEditor extends Canvas
this.container = dom('div', { class: 'absolute top-0 left-0 overflow-hidden w-full h-full touch-none', listeners: { mousedown: () => { this.selection.clear(); this.updateSelection() } } }, [
dom('div', { class: 'flex flex-col absolute sm:top-2 top-10 left-2 z-[35] overflow-hidden gap-4' }, [
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom avant')], class: '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'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.reset() } }, [icon('radix-icons:corners')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Tout contenir')], class: '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'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Zoom arrière')], class: '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'
}),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom * 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:plus')]), 'Zoom avant', 'right'),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.reset() } }, [icon('radix-icons:corners')]), 'Tout contenir', 'right'),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.zoomTo(this._x, this._y, clamp(this._zoom / 1.1, this.containZoom, Canvas.maxZoom)) } }, [icon('radix-icons:minus')]), 'Zoom arrière', 'right'),
]),
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('ph:arrow-bend-up-left')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Annuler (Ctrl+Z)')], class: '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'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('ph:arrow-bend-up-right')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Rétablir (Ctrl+Y)')], class: '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'
}),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.undo() } }, [icon('ph:arrow-bend-up-left')]), 'Annuler (Ctrl+Z)', 'right'),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => this.history.redo() } }, [icon('ph:arrow-bend-up-right')]), 'Rétablir (Ctrl+Y)', 'right'),
]),
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:gear')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Préférences')], class: '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'
}),
popper(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:question-mark-circled')]), {
delay: 120, arrow: true, placement: 'right', offset: 8, content: [text('Aide')], class: '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'
}),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:gear')]), 'Préférences', 'right'),
tooltip(dom('span', { class: 'w-8 h-8 flex justify-center items-center p-2 hover:bg-light-30 dark:hover:bg-dark-30 cursor-pointer', listeners: { click: () => {} } }, [icon('radix-icons:question-mark-circled')]), 'Aide', 'right'),
]),
]), this.pattern, this.transform
]);

View File

@ -850,7 +850,7 @@
"5d7u2jvi4u0nnrzesderha3uo8kb3zjq"
],
"4": [
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3"
"4cm4mz365yupl9h8nfet7orbapeg2fzn"
],
"5": [
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w"
@ -6082,7 +6082,7 @@
]
},
"3ugv3ym7bswjhz0drbx6v3932q7w3qsy": {
"description": "Vous apprenez le sort unique [[1. Règles/4. La magie/2. Liste des sorts#^068b55|Soin]]. #todo\n+3 mana max.",
"description": "Vous apprenez le sort unique [[1. Règles/4. La magie/2. Liste des sorts#^068b55|Soin]].\n+3 mana max.",
"id": "3ugv3ym7bswjhz0drbx6v3932q7w3qsy",
"effect": [
{
@ -6091,6 +6091,13 @@
"property": "mana",
"operation": "add",
"value": 3
},
{
"id": "mxshb0udl4zahch8l9v1rtpm1d5fv4au",
"category": "list",
"list": "spells",
"action": "add",
"item": "zltvtru98sm0ad9whiw5tty0gy4q2jur"
}
]
},
@ -6193,9 +6200,17 @@
]
},
"iaaoqrn6kgvovzfpk4ygyd8yjwrxkml3": {
"description": "Vous apprenez le sort unique [[2. Liste des sorts#Sorts unique|Focalisation destructrice]]. #todo",
"description": "Vous apprenez le sort unique [[2. Liste des sorts#Sorts unique|Focalisation destructrice]].",
"id": "iaaoqrn6kgvovzfpk4ygyd8yjwrxkml3",
"effect": []
"effect": [
{
"id": "96xafpi1s4baffwht7gvfx7eqx3tzocq",
"category": "list",
"list": "spells",
"action": "add",
"item": "kd84l3gujh4evsyriti4g9sk1zwbxu8d"
}
]
},
"syj4q2o1qfh5vezi2d8bzgs9fn9ok273": {
"description": "Lorsque vous voyez un sort être lancé, vous pouvez [[2. Actions en combat#Saisir une opportunité|saisir l'opportunité]] et dépenser jusqu'à 5 points de mana pour imposer un malus de égal au mana dépensé.",
@ -6434,7 +6449,7 @@
]
},
"yn70y3tmxdo1w0zvm7at1i7uretcgkvk": {
"description": "Le maximum de toutes les compétences augmente de 1 point. #todo\n+2 points de compétence.",
"description": "Le maximum de toutes les compétences augmente de 1 point.\n+2 points de compétence.",
"id": "yn70y3tmxdo1w0zvm7at1i7uretcgkvk",
"effect": [
{
@ -6450,11 +6465,130 @@
"property": "modifier/curiosity",
"operation": "add",
"value": 1
},
{
"id": "ujohx9nf79xp2pu7db3m96cq7u6whhom",
"category": "value",
"property": "bonus/abilities/athletics",
"operation": "add",
"value": 1
},
{
"id": "52lwifstredyutwzev0aospih6yqfkk0",
"category": "value",
"property": "bonus/abilities/acrobatics",
"operation": "add",
"value": 1
},
{
"id": "tpp9gxgsxji4tqhvz9wt2qw4jbqi0vsj",
"category": "value",
"property": "bonus/abilities/intimidation",
"operation": "add",
"value": 1
},
{
"id": "tautma08r6qh6kd7g2x9qye04ke6jezu",
"category": "value",
"property": "bonus/abilities/sleightofhand",
"operation": "add",
"value": 1
},
{
"id": "v2tmzc6w5ol1sxktqu16qegw7net3gsa",
"category": "value",
"property": "bonus/abilities/stealth",
"operation": "add",
"value": 1
},
{
"id": "gdefm1aqperjd6jbzjdj9njudiuehpt8",
"category": "value",
"property": "bonus/abilities/survival",
"operation": "add",
"value": 1
},
{
"id": "lqfs22jlrc0iq6vqy4nrart3og7vdbzq",
"category": "value",
"property": "bonus/abilities/investigation",
"operation": "add",
"value": 1
},
{
"id": "hhjsudmrlx346avj8kqbz8w1qd78xtu4",
"category": "value",
"property": "bonus/abilities/history",
"operation": "add",
"value": 1
},
{
"id": "0ryjnvdv7ube7stshuf7z6ecqfmgbxn5",
"category": "value",
"property": "bonus/abilities/religion",
"operation": "add",
"value": 1
},
{
"id": "je0ly1u7ztoyhpik3nabnpm8c26brial",
"category": "value",
"property": "bonus/abilities/arcana",
"operation": "add",
"value": 1
},
{
"id": "3jfe6nk5b1feghtar4xjaqqb3680kgv1",
"category": "value",
"property": "bonus/abilities/understanding",
"operation": "add",
"value": 1
},
{
"id": "t8wlpfa84jt90gs156dq37fk3bvjxb1r",
"category": "value",
"property": "bonus/abilities/perception",
"operation": "add",
"value": 1
},
{
"id": "fje9o5wskdqm5xhq53tgtm1os8sowktx",
"category": "value",
"property": "bonus/abilities/performance",
"operation": "add",
"value": 1
},
{
"id": "v5aptp9kp274mstx58fwtnddkig92la1",
"category": "value",
"property": "bonus/abilities/medecine",
"operation": "add",
"value": 1
},
{
"id": "opcasr4pbwgetjqwslhlhxn3d8qxxqvi",
"category": "value",
"property": "bonus/abilities/persuasion",
"operation": "add",
"value": 1
},
{
"id": "gg30nbm489tf4850r7fwwykb97y8ly8j",
"category": "value",
"property": "bonus/abilities/animalhandling",
"operation": "add",
"value": 1
},
{
"id": "lzi81y8vkdztgcv9tjja8klx2mpq8os2",
"category": "value",
"property": "bonus/abilities/deception",
"operation": "add",
"value": 1
}
]
},
"nqvexvg3ui6w2hqknwrw4pvf5axvu4go": {
"description": "Le maximum de toutes les compétences est de 6 points, sauf s'il est déjà supérieur. #todo\n+2 points de compétence.",
"description": "Le maximum de toutes les compétences est de 6 points, sauf s'il est déjà supérieur.\n+2 points de compétence.",
"id": "nqvexvg3ui6w2hqknwrw4pvf5axvu4go",
"effect": [
{
@ -6470,6 +6604,125 @@
"property": "modifier/curiosity",
"operation": "add",
"value": 1
},
{
"id": "2fbjzp4cg6mobxhycvq5ljnah1f1dz7y",
"category": "value",
"property": "bonus/abilities/athletics",
"operation": "min",
"value": 6
},
{
"id": "jrm0xt17xbpkg6fgp0d54qu7qmfp5ftr",
"category": "value",
"property": "bonus/abilities/acrobatics",
"operation": "min",
"value": 6
},
{
"id": "lra7cbvx5tqj5m5ql4to05x6slknf1wm",
"category": "value",
"property": "bonus/abilities/intimidation",
"operation": "min",
"value": 6
},
{
"id": "cik0xa4k7gcepj4wcab710ixhfwaxd3h",
"category": "value",
"property": "bonus/abilities/sleightofhand",
"operation": "min",
"value": 6
},
{
"id": "4vieiuimshc3nyvr95fnktuu6n09yjho",
"category": "value",
"property": "bonus/abilities/stealth",
"operation": "min",
"value": 6
},
{
"id": "6gb2ax95ig3yzmgabpbpc5ogv8pct76t",
"category": "value",
"property": "bonus/abilities/survival",
"operation": "min",
"value": 6
},
{
"id": "zoxh7gpp693hvfokwnfdu3d6dii4rpr8",
"category": "value",
"property": "bonus/abilities/investigation",
"operation": "min",
"value": 6
},
{
"id": "h2ebi472yx3zn8bdhli0ydkoc3kj80wh",
"category": "value",
"property": "bonus/abilities/history",
"operation": "min",
"value": 6
},
{
"id": "cmnafy5kt7w0w7e3lj1zgyg65oubb9d3",
"category": "value",
"property": "bonus/abilities/religion",
"operation": "min",
"value": 6
},
{
"id": "gniqvw47aukyd2tl3p4vzlabopdb1gu9",
"category": "value",
"property": "bonus/abilities/arcana",
"operation": "min",
"value": 6
},
{
"id": "39l4ho7y3m9quzwqtasfmbz9k4xsiwvl",
"category": "value",
"property": "bonus/abilities/understanding",
"operation": "min",
"value": 6
},
{
"id": "z8kw81jtizyw6m57adyl2vat6h7c86r5",
"category": "value",
"property": "bonus/abilities/perception",
"operation": "min",
"value": 6
},
{
"id": "5xsg3w22xai41q5lf5akxt3u0xnxi54b",
"category": "value",
"property": "bonus/abilities/performance",
"operation": "min",
"value": 6
},
{
"id": "9k1l0wo3k9qymghh1ghf74gc1xw95y4x",
"category": "value",
"property": "bonus/abilities/medecine",
"operation": "min",
"value": 6
},
{
"id": "rj0icbs2tqt2hnxddtr6zv0z7uls1eq2",
"category": "value",
"property": "bonus/abilities/persuasion",
"operation": "min",
"value": 6
},
{
"id": "tjq16rz76nh56i9636dygqvk81906goe",
"category": "value",
"property": "bonus/abilities/animalhandling",
"operation": "min",
"value": 6
},
{
"id": "ts20cerlj4qior7mzzbtg2k882qymrsl",
"category": "value",
"property": "bonus/abilities/deception",
"operation": "min",
"value": 6
}
]
},
@ -7903,7 +8156,7 @@
]
},
"s5kidncgfzw85ffubl718lx2f68suhqf": {
"description": "Votre connexion innée avec la magie vous a bénie d'un don pour cet art. Choisissez une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]]. Vous gagnez le premier niveau de cette branche. #todo",
"description": "Votre connexion innée avec la magie vous a bénie d'un don pour cet art. Choisissez une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]]. Vous gagnez le premier niveau de cette branche.",
"id": "s5kidncgfzw85ffubl718lx2f68suhqf",
"effect": [
{
@ -8072,7 +8325,7 @@
]
},
"qf3eru17f8u3hysq56k246mlq7p2rbc9": {
"description": "Vous gagnez un niveau dans une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]] dans laquelle vous avez déjà au moins un niveau. #todo",
"description": "Vous gagnez un niveau dans une branche de l'[[1. Les évolutions de valeur.canvas#L'arbre de magie|arbre de magie]] dans laquelle vous avez déjà au moins un niveau.",
"id": "qf3eru17f8u3hysq56k246mlq7p2rbc9",
"effect": [
{
@ -8358,9 +8611,17 @@
]
},
"sq43lzc8bdftrbbfwaq5l6nx1h5jx0eh": {
"description": "Vous apprenez le sort unique [[2. Liste des sorts#^5b38b6|Domination mentale]]. #todo",
"description": "Vous apprenez le sort unique [[2. Liste des sorts#^5b38b6|Domination mentale]].",
"id": "sq43lzc8bdftrbbfwaq5l6nx1h5jx0eh",
"effect": []
"effect": [
{
"id": "y7gnxzaf9puprd4ij56wk7bq8xjnt21d",
"category": "list",
"list": "spells",
"action": "add",
"item": "wj2rxkbw85zd9st8k2w3eezqc1naoy5g"
}
]
},
"c4nptbfb5uoyz98ovsqjxwlssgui9h9p": {
"description": "Vous êtes capable d'utiliser les particularités magiques de votre Aspect sans vous transformer.",
@ -9619,11 +9880,6 @@
"description": "suis",
"effect": []
},
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3": {
"id": "8w4jthjrn3l8u4trmj46z6t6ab5rbgk3",
"description": "Nicolas",
"effect": []
},
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w": {
"id": "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w",
"description": "Sarkozy",
@ -9703,6 +9959,11 @@
"id": "qcp28eysi3l3n438v41kowisdpq4ht61",
"description": "",
"effect": []
},
"4cm4mz365yupl9h8nfet7orbapeg2fzn": {
"id": "4cm4mz365yupl9h8nfet7orbapeg2fzn",
"description": "Nicolas",
"effect": []
}
}
}

View File

@ -4,7 +4,7 @@ import characterConfig from '#shared/character-config.json';
import { fakeA } from "#shared/proses";
import { button, input, loading, numberpicker, select, toggle } from "#shared/components.util";
import { div, dom, icon, mergeClasses, text, type Class } from "#shared/dom.util";
import { followermenu, popper } from "#shared/floating.util";
import { followermenu, popper, tooltip } from "#shared/floating.util";
import { clamp } from "#shared/general.util";
import markdownUtil from "#shared/markdown.util";
@ -34,7 +34,7 @@ export const defaultCharacter: Character = {
health: 0,
mana: 0,
spells: [],
equipment: [],
items: [],
exhaustion: 0,
sickness: [],
},
@ -198,8 +198,8 @@ export const CharacterValidation = z.object({
thumbnail: z.any(),
});
type Property = { value: number | string | false, id: string, operation: "set" | "add" };
type PropertySum = { list: Array<Property>, value: number, _dirty: boolean };
type Property = { value: number | string | false, id: string, operation: "set" | "add" | "min" };
type PropertySum = { list: Array<Property>, min: number, value: number, _dirty: boolean };
export class CharacterCompiler
{
protected _character!: Character;
@ -285,7 +285,7 @@ export class CharacterCompiler
return;
case "value":
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
@ -318,7 +318,7 @@ export class CharacterCompiler
return;
case "value":
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id), 1);
@ -375,6 +375,8 @@ export class CharacterCompiler
sum += modifier.value;
else if(buffer.list[i]?.operation === 'set')
sum = modifier.value;
else if(buffer.list[i]?.operation === 'min')
this._buffer[property]!.min = modifier.value;
}
}
else
@ -383,6 +385,8 @@ export class CharacterCompiler
sum += buffer.list[i]!.value as number;
else if(buffer.list[i]?.operation === 'set')
sum = buffer.list[i]!.value as number;
else if(buffer.list[i]?.operation === 'min')
this._buffer[property]!.min = buffer.list[i]!.value as number;
}
}
@ -393,9 +397,9 @@ export class CharacterCompiler
const object = path.length === 1 ? this._result : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, this._result as any);
if(object.hasOwnProperty(path.slice(-1)[0]!))
object[path.slice(-1)[0]!] = sum;
object[path.slice(-1)[0]!] = Math.max(sum, this._buffer[property]!.min);
this._buffer[property]!.value = sum;
this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min);
this._buffer[property]!._dirty = false;
}
}
@ -423,7 +427,7 @@ export class CharacterBuilder extends CharacterCompiler
useRequestFetch()(`/api/character/${id}`).then(character => {
if(character)
{
this._character = character;
this.character = character;
document.title = `d[any] - Edition de ${character.name ?? 'nouveau personnage'}`;
load.remove();
@ -459,13 +463,7 @@ export class CharacterBuilder extends CharacterCompiler
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 }), {
arrow: true,
offset: 8,
content: [ this._helperText ],
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"
}) ]),
div(), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div(undefined, [ tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]),
]),
this._content,
]));
@ -683,6 +681,13 @@ export class PickableFeature
return this._content;
}
}
class FeatureTable
{
constructor(table: Record<number, FeatureID[]>)
{
}
}
abstract class BuilderTab {
protected _builder: CharacterBuilder;
protected _content!: Array<Node | string>;
@ -741,6 +746,8 @@ class PeoplePicker extends BuilderTab
]),
button(text('Suivant'), () => this._builder.display(1), 'h-[35px] px-[15px]'),
]), div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options)];
this.update();
}
override update()
{
@ -798,6 +805,8 @@ class LevelPicker extends BuilderTab
]),
button(text('Suivant'), () => this._builder.display(2), 'h-[35px] px-[15px]'),
]), div('flex flex-col flex-1 gap-4 mx-8 my-4', this._options.flatMap(e => [...e]))];
this.update();
}
override update()
{
@ -883,6 +892,8 @@ class TrainingPicker extends BuilderTab
]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])];
this.switchTab(0);
this.update();
}
switchTab(tab: number)
{
@ -962,7 +973,7 @@ class AbilityPicker extends BuilderTab
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._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', [ numberInput(this._builder.character.abilities[e], (value) => {
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);
@ -975,13 +986,7 @@ class AbilityPicker extends BuilderTab
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('')) ]
})]),
}}), tooltip(pushAndReturn(this._maxs, dom('span', { class: 'text-lg text-end cursor-pointer', text: '' })), pushAndReturn(this._tooltips, text('')), 'bottom-end')]),
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" }),
]));
@ -993,6 +998,8 @@ class AbilityPicker extends BuilderTab
]),
button(text('Suivant'), () => this._builder.display(4), 'h-[35px] px-[15px]'),
]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48', this._options)];
this.update();
}
override update()
{
@ -1002,13 +1009,11 @@ class AbilityPicker extends BuilderTab
this._pointsInput.value = ((values.ability ?? 0) - abilities).toString();
ABILITIES.forEach((e, i) => {
const max = (values[`abilities/${e}/max`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0);
const max = (values[`bonus/abilities/${e}`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0);
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._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[`bonus/abilities/${e}`] ?? 0}`;
this._maxs[i]!.textContent = `/ ${max ?? 0}`;
return this._builder.character.abilities[e];
})
}
static override validate(builder: CharacterBuilder): boolean
@ -1091,6 +1096,8 @@ class AspectPicker extends BuilderTab
]),
button(text('Enregistrer'), () => this._builder.save(), 'h-[35px] px-[15px]'),
]), div('flex flex-row flex-wrap justify-center items-center flex-1 gap-8 mx-8 my-4 px-8', this._options)];
this.update();
}
override update()
{

View File

@ -20,6 +20,19 @@ export function loading(size: 'small' | 'normal' | 'large' = 'normal'): HTMLElem
{
return dom('span', { class: ["after:block after:relative after:rounded-full after:border-transparent after:border-t-accent-purple after:animate-spin", {'w-6 h-6 border-4 border-transparent after:-top-[4px] after:-left-[4px] after:w-6 after:h-6 after:border-4': size === 'normal', 'w-4 h-4 after:-top-[2px] after:-left-[2px] after:w-4 after:h-4 after:border-2': size === 'small', 'w-12 h-12 after:-top-[6px] after:-left-[6px] after:w-12 after:h-12 after:border-[6px]': size === 'large'}] })
}
export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise<HTMLElement>): HTMLElement
{
const load = loading(size);
fn.then((element) => {
load.replaceWith(element);
}).catch(e => {
console.error(e);
load.remove();
})
return load;
}
export function button(content: Node, onClick?: () => void, cls?: Class)
{
return dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none

View File

@ -1,11 +1,11 @@
import { safeDestr as parse } from 'destr';
import { Canvas, CanvasEditor } from "#shared/canvas.util";
import render from "#shared/markdown.util";
import { confirm, contextmenu, popper } from "#shared/floating.util";
import { confirm, contextmenu, tooltip } from "#shared/floating.util";
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
import { loading } from "#shared/components.util";
import prose, { h1, h2 } from "#shared/proses";
import { getID, ID_SIZE, parsePath } from '#shared/general.util';
import { getID, parsePath } from '#shared/general.util';
import { TreeDOM, type Recursive } from '#shared/tree';
import { History } from '#shared/history.util';
import { MarkdownEditor } from '#shared/editor.util';
@ -681,15 +681,15 @@ export class Editor
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
popper(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), { delay: 150, offset: 8, placement: 'left', arrow: true, content: [text('Navigable')], class: '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' }),
popper(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: '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' }),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
])]);
}, (item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
popper(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), { delay: 150, offset: 8, placement: 'left', arrow: true, content: [text('Navigable')], class: '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' }),
popper(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: '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' }),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
])]);
});
@ -714,7 +714,7 @@ export class Editor
private add(type: FileType, nextTo: Recursive<LocalContent>)
{
const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length;
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(ID_SIZE), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
}
private remove(item: LocalContent & { element?: HTMLElement })

View File

@ -6,7 +6,7 @@ import { button, combobox, foldable, input, multiselect, numberpicker, select, t
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
import { ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
import characterConfig from "#shared/character-config.json";
import { getID, ID_SIZE } from "#shared/general.util";
import { getID } from "#shared/general.util";
import renderMarkdown, { renderText } from "#shared/markdown.util";
import { Tree } from "#shared/tree";
import markdownUtil from "#shared/markdown.util";
@ -106,12 +106,12 @@ class PeopleEditor extends BuilderTab
const add = () => {
const people: RaceConfig = {
id: getID(ID_SIZE),
id: getID(),
name: '',
description: '',
options: LEVELS.map(e => {
const feature: Feature = {
id: getID(ID_SIZE),
id: getID(),
description: '',
effect: [],
}
@ -143,7 +143,7 @@ class PeopleEditor extends BuilderTab
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
const _feature: Feature = { id: getID(), description: '', effect: [] };
config.features[_feature.id] = _feature;
config.peoples[people]!.options[level]!.push(_feature.id);
element.parentElement?.appendChild(render(people, level, _feature.id));
@ -190,7 +190,7 @@ class TrainingEditor extends BuilderTab
const context = contextmenu(e.clientX, e.clientY, [
dom('div', { class: 'px-2 py-1 border-bottom border-light-35 dark:border-dark-35 cursor-pointer hover:bg-light-40 dark:hover:bg-dark-40 text-light-100 dark:text-dark-100', listeners: { click: () => {
context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
const _feature: Feature = { id: getID(), description: '', effect: [] };
config.features[_feature.id] = _feature;
config.training[stat][level].push(_feature.id);
element.parentElement?.appendChild(render(stat, level, _feature.id));
@ -326,7 +326,7 @@ class SpellEditor extends BuilderTab
}
const add = () => {
config.spells.push({
id: getID(ID_SIZE),
id: getID(),
name: '',
rank: 1,
type: 'precision',
@ -404,7 +404,7 @@ export class FeatureEditor
div('flex flex-row justify-between', [
dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => {
this._table.appendChild(this._edit({ id: getID(ID_SIZE) }));
this._table.appendChild(this._edit({ id: getID() }));
}, 'p-1'), 'Ajouter', 'left'),
]),
this._table,
@ -430,7 +430,7 @@ export class FeatureEditor
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-center', [
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);
content.replaceWith(this._edit(effect));
}, 'p-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifieur", "bottom"), tooltip(button(icon('radix-icons:trash'), () => {
this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id);
content.remove();
@ -484,7 +484,7 @@ export class FeatureEditor
tooltip(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 newValueSelection = valueVariable();
valueSelection?.parentElement?.replaceChild(newValueSelection, valueSelection);
valueSelection.replaceWith(newValueSelection);
valueSelection = newValueSelection;
summaryText.textContent = textFromEffect(buffer);
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Changer d\'editeur', 'bottom'),
@ -514,13 +514,13 @@ export class FeatureEditor
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';
const element = redraw();
content?.parentElement?.replaceChild(element, content);
content.replaceWith(element);
content = element;
}, 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 };
const option: Extract<FeatureItem, { category: 'choice' }>["options"][number] = { id: getID(), category: 'value', text: '', operation: 'add', property: '', value: 0 };
(buffer as Extract<FeatureItem, { category: 'choice' }>).options.push(option);
list.appendChild(render(option, true));
};
@ -529,7 +529,7 @@ export class FeatureEditor
const combo = combobox([...featureChoices].filter(e => (e?.value as FeatureItem)?.category !== 'choice').map(e => { if(e) e.value = Array.isArray(e.value) ? e.value.filter(f => (f?.value as FeatureItem)?.category !== 'choice') : e.value; return e; }), { defaultValue: match(option), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => {
option = { id: option.id, ...e } as FeatureEffect & { text: string };
const element = render(option, true);
_content?.parentElement?.replaceChild(element, _content);
_content.replaceWith(element);
_content = element;
} });
let _content: HTMLElement = foldable(_bottom, [ div('flex flex-1 justify-between', [ div('flex flex-1 flex-row',[ combo, ..._top, input('text', { defaultValue: option.text, input: (value) => option.text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] flex-shrink-1', placeholder: 'Description' }) ]), tooltip(button(icon('radix-icons:trash'), () => {
@ -553,7 +553,7 @@ export class FeatureEditor
combobox(featureChoices, { defaultValue: match(_buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[300px] -m-px hover:z-10 h-[36px]' }, fill: 'cover', change: (e) => {
_buffer = { id: _buffer.id, ...e } as FeatureItem;
const element = redraw();
content?.parentElement?.replaceChild(element, content);
content.replaceWith(element);
content = element;
} }),
...top,
@ -605,7 +605,10 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Arbre de magie (Elements)', value: { category: 'value', property: 'mastery/magicelement', operation: 'add', value: 1 } },
{ text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', operation: 'add', value: 1 } }
] },
{ text: 'Compétence', value: Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) },
{ text: 'Compétences', value: [
...Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
{ text: 'Max de compétence', value: Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
] },
{ text: 'Modifieur', value: [
{ text: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } },
{ text: 'Modifieur de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } },
@ -614,7 +617,6 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Modifieur de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } },
{ text: 'Modifieur de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } },
{ text: 'Modifieur de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } },
//@ts-ignore
{ text: 'Modifieur au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [
{ text: 'Modifieur de force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 },
{ text: 'Modifieur de dextérité', category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 },
@ -623,7 +625,7 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Modifieur de curiosité', category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 },
{ text: 'Modifieur de charisme', category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 },
{ text: 'Modifieur de psyché', egory: 'value', property: 'modifier/psyche', operation: 'add', value: 1 }
]}}
]} as Partial<FeatureItem>}
] },
{ text: 'Jet de résistance', value: [
{ text: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } },
@ -633,7 +635,6 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 } },
{ text: 'Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } },
{ text: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } },
//@ts-ignore
{ text: 'Résistance au choix', value: { category: 'choice', text: '+1 au jet de résistance de ', options: [
{ text: 'Force', category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 },
{ text: 'Dextérité', category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 },
@ -642,7 +643,7 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Curiosité', category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 },
{ text: 'Charisme', category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 },
{ text: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }
]}}
]} as Partial<FeatureItem>}
] },
{ text: 'Bonus', value: Object.keys(config.resistances).map((e: Resistance) => ({ text: config.resistances[e]!.name, value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) },
{ text: 'Rang', value: [
@ -760,12 +761,14 @@ function textFromEffect(effect: Partial<FeatureItem>): string
{
case 'resistance':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise for = interdit).' });
case 'abilities':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${config.abilities[splited[2] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${config.abilities[splited[2] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${config.abilities[splited[2] as Ability].name} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${config.abilities[splited[2] as Ability].name} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${config.abilities[splited[2] as Ability].name} max = interdit).` });
default: return 'Bonus inconnu';
}
case 'resistance':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${config.resistances[splited[1] as Resistance]!.name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${config.resistances[splited[1] as Resistance]!.name} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (${config.resistances[splited[1] as Resistance]!.name} = interdit).` });
case 'abilities':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${`${config.abilities[splited[1] as Ability].name}.`}` });
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${config.abilities[splited[1] as Ability].name}.` });
case 'modifier':
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+' }, suffix: { truely: ` au mod. de ${mainStatTexts[splited[1] as MainStat]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Mod. de ${mainStatTexts[splited[1] as MainStat]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Mod. de ${mainStatShortTexts[splited[1] as MainStat]} = interdit).` });
default: break;

View File

@ -19,11 +19,11 @@ export interface FollowerProperties extends FloatingProperties
}
export interface PopperProperties extends FloatingProperties
{
content?: NodeChildren;
content?: NodeChildren | (() => NodeChildren);
delay?: number;
onShow?: (element: HTMLDivElement) => boolean | void;
onHide?: (element: HTMLDivElement) => boolean | void;
onShow?: () => boolean | void;
onHide?: () => boolean | void;
}
export interface ModalProperties
@ -43,7 +43,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
{
let shown = false, timeout: Timer;
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "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 content = dom('div', { class: ['fixed hidden', properties?.class], style: properties?.style, attributes: { 'data-state': 'closed' } });
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
function update()
@ -53,7 +53,6 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
strategy: 'fixed',
middleware: [
properties?.offset ? FloatingUI.offset(properties?.offset) : undefined,
FloatingUI.hide({ rootBoundary: rect, strategy: "escaped" }),
FloatingUI.hide({ rootBoundary: rect }),
FloatingUI.shift({ rootBoundary: rect }),
FloatingUI.flip({ rootBoundary: rect }),
@ -109,8 +108,17 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
let stop: () => void | undefined;
function show()
{
if(shown || !properties?.onShow || properties?.onShow(content) !== false)
if(shown || !properties?.onShow || properties?.onShow() !== false)
{
if(typeof properties?.content === 'function')
{
properties.content = properties.content();
}
if(content.children.length === 0 && (properties?.content && properties.content.length > 0 || properties?.arrow))
{
content.replaceChildren(...(properties!.content as Node[]), arrow);
}
clearTimeout(timeout);
timeout = setTimeout(() => {
@ -137,7 +145,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
function hide()
{
if(!properties?.onHide || properties?.onHide(content) !== false)
if(!properties?.onHide || properties?.onHide() !== false)
{
clearTimeout(timeout);
@ -274,13 +282,13 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
},
}, content, properties);
}
export function tooltip(container: HTMLElement, txt: string, placement: FloatingUI.Placement, delay?: number): HTMLElement
export function tooltip(container: HTMLElement, txt: string | Text, placement: FloatingUI.Placement, delay?: number): HTMLElement
{
return popper(container, {
arrow: true,
offset: 8,
delay: delay,
content: [ text(txt) ],
content: [ typeof txt === 'string' ? text(txt) : txt ],
placement: placement,
class: "fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50"
});

View File

@ -1,12 +1,12 @@
export const ID_SIZE = 32;
const ID_SIZE = 32;
export function unifySlug(slug: string | string[]): string
{
return (Array.isArray(slug) ? slug.join('/') : slug);
}
export function getID(length: number)
export function getID()
{
for (var id = [], i = 0; i < length; i++)
for (var id = [], i = 0; i < ID_SIZE; i++)
id.push((36 * Math.random() | 0).toString(36));
return id.join("");
}

View File

@ -1,9 +1,9 @@
import type { CharacterConfig } from "~/types/character";
import type { CharacterConfig, i18nID } from "~/types/character";
import characterConfig from '#shared/character-config.json';
const config = characterConfig as CharacterConfig;
export function getText(id?: string, lang?: string)
export function getText(id?: i18nID, lang?: string)
{
return id ? (config.texts.hasOwnProperty(id) ? config.texts[id][lang ?? "default"] : '') : undefined;
}

View File

@ -4,7 +4,7 @@ import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td
import { heading } from "hast-util-heading";
import { headingRank } from "hast-util-heading-rank";
import { parseId } from "#shared/general.util";
import { loading } from "#shared/components.util";
import { async, loading } from "#shared/components.util";
export function renderMarkdown(markdown: Root, proses: Record<string, Prose>): HTMLDivElement
{
@ -45,35 +45,29 @@ export interface MDProperties
}
export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
{
const load = loading('normal');
queueMicrotask(() => {
useMarkdown().parse(content).then(data => {
if(filter)
{
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
if(start !== -1)
{
let end = start;
const rank = headingRank(data.children[start])!;
while(end < data.children.length)
{
end++;
if(heading(data.children[end]) && headingRank(data.children[end])! <= rank)
break;
}
data = { ...data, children: data.children.slice(start, end) };
}
}
const el = renderMarkdown(data, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
if(properties) styling(el, properties);
return async('large', useMarkdown().parse(content).then(data => {
if(filter)
{
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
load.parentElement?.replaceChild(el, load);
});
})
return load;
if(start !== -1)
{
let end = start;
const rank = headingRank(data.children[start])!;
while(end < data.children.length)
{
end++;
if(heading(data.children[end]) && headingRank(data.children[end])! <= rank)
break;
}
data = { ...data, children: data.children.slice(start, end) };
}
}
const el = renderMarkdown(data, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
if(properties) styling(el, properties);
return el;
}));
}

View File

@ -1,11 +1,11 @@
import { dom, icon, type NodeChildren, type Node } from "#shared/dom.util";
import { dom, icon, type NodeChildren, type Node, div } from "#shared/dom.util";
import { parseURL } from 'ufo';
import render from "#shared/markdown.util";
import { popper } from "#shared/floating.util";
import { Canvas } from "#shared/canvas.util";
import { Content, iconByType, type LocalContent } from "#shared/content.util";
import { parsePath, unifySlug } from "#shared/general.util";
import { loading } from "./components.util";
import { async, loading } from "./components.util";
export type CustomProse = (properties: any, children: NodeChildren) => Node;
@ -20,8 +20,6 @@ export const a: Prose = {
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
let rendered = false;
const el = dom('a', { class: 'text-accent-blue inline-flex items-center', attributes: { href: nav.href }, listeners: {
'click': (e) => {
e.preventDefault();
@ -43,26 +41,22 @@ export const a: Prose = {
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 overflow-auto z-[45]',
content: [loading("large")],
viewport: document.getElementById('mainContainer') ?? undefined,
onShow(content: HTMLDivElement) {
if(!rendered)
{
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: 'py-4 px-6' }), content.children[0]!);
}
if(_content?.type === 'canvas')
{
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!);
canvas.mount();
}
});
rendered = true;
}
content: () => {
return [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown')
{
return 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' });
}
if(_content?.type === 'canvas')
{
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
queueMicrotask(() => canvas.mount());
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
}
return div('');
}))];
},
viewport: document.getElementById('mainContainer') ?? undefined
});
}
@ -95,24 +89,24 @@ export const fakeA: Prose = {
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")],
onShow(content: HTMLDivElement) {
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
return false;
content.replaceChild(loading("large"), content.children[0]!);
Content.getContent(overview.id).then((_content) => {
content: () => {
return [async('large', 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]!);
return 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' });
}
if(_content?.type === 'canvas')
{
const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!);
canvas.mount();
queueMicrotask(() => canvas.mount());
return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
}
});
return div('');
}))];
},
onShow() {
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
return false;
},
});
}

57
types/character.d.ts vendored
View File

@ -10,11 +10,9 @@ export type SpellElement = typeof SPELL_ELEMENTS[number];
export type Alignment = typeof ALIGNMENTS[number];
export type FeatureID = string;
export type TextID = string;
export type i18nID = string;
export type Resistance = string;
export type Dice = `${number}d${4 | 6 | 8 | 10 | 12 | 20}`;
export type Character = {
id: number;
@ -41,7 +39,15 @@ export type CharacterVariables = {
sickness: Array<{ id: string, state: number | true }>;
spells: string[]; //Spell ID
equipment: string[]; //Equipment ID
items: ItemState[];
};
type ItemState = {
id: string,
amount: number;
enchantments?: [];
charges?: number;
equipped?: boolean;
state?: any;
};
export type CharacterConfig = {
peoples: Record<string, RaceConfig>;
@ -51,35 +57,44 @@ export type CharacterConfig = {
spells: SpellConfig[];
aspects: AspectConfig[];
features: Record<FeatureID, Feature>;
enchantments: Record<string, { name: string, effect: FeatureEffect[] }>; //TODO
items: Record<string, ItemConfig & { enchantments: string[] }>;
enchantments: Record<string, { name: string, effect: FeatureEffect[], power: number }>; //TODO
items: Record<string, ItemConfig>;
lists: Record<string, { id: string, name: string, [key: string]: any }[]>;
texts: Record<TextID, Localized>;
texts: Record<i18nID, Localized>;
};
export type ItemConfig = { id: string, weight?: number, price?: number, power: number } & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig);
export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig);
type CommonItemConfig = {
id: string;
rarity: 'common' | 'uncommon' | 'rare' | 'legendary';
weight?: number; //Optionnal but highly recommended
price?: number; //Optionnal but highly recommended
power?: number; //Optionnal as most mundane items should not receive enchantments (potions, herbal heals, etc...)
charge?: number //Max amount of charges
equippable: boolean;
}
type ArmorConfig = {
category: 'armor';
name: string; //TODO -> TextID
description: TextID;
life: number;
absorb: number;
description: i18nID;
health: number;
absorb: { static: number, percent: number };
};
type WeaponConfig = {
category: 'armor';
category: 'weapon';
name: string; //TODO -> TextID
description: TextID;
damage: Dice;
description: i18nID;
damage: string; //Dice formula
};
type WondrousConfig = {
category: 'armor';
category: 'wondrous';
name: string; //TODO -> TextID
description: TextID;
description: i18nID;
effect: FeatureEffect[];
};
type MundaneConfig = {
category: 'armor';
category: 'mundane';
name: string; //TODO -> TextID
description: TextID;
description: i18nID;
};
export type SpellConfig = {
id: string;
@ -120,7 +135,7 @@ export type AspectConfig = {
export type FeatureEffect = {
id: FeatureID;
category: "value";
operation: "add" | "set";
operation: "add" | "set" | "min";
property: string;
value: number | `modifier/${MainStat}` | false;
} | {
@ -140,10 +155,10 @@ export type FeatureItem = FeatureEffect | {
exclusive: boolean; //Disallow to pick the same option twice
};
options: Array<FeatureEffect & { text: string }>;
}
};
export type Feature = {
id: FeatureID;
description: string;
description: i18nID;
effect: FeatureItem[];
};