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 { Content, iconByType } from '#shared/content.util';
import { dom, icon, text } from '#shared/dom.util'; import { dom, icon, text } from '#shared/dom.util';
import { unifySlug } from '#shared/general.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'; import { link } from '#shared/components.util';
const options = ref<DropdownOption[]>([{ 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 } }, [ 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` } }), 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 } }), 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) => { }, (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, [ 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 }), 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 } }), 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); }, (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)); (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> </div>
<div class="flex flex-1 px-8"> <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"> <div class="flex flex-col">
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise d'arme</span> <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-2 gap-x-3 gap-y-1"> <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 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 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" /> <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" 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" /> <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> <div v-if="character.mastery.armor > 0" class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
<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">
<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 > 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 > 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" /> <PreviewA v-if="character.mastery.armor > 2" href="regles/annexes/equipement#Les armures lourdes" label="Armure lourde" />
</div> </div>
</div> <div class="grid grid-cols-2 gap-x-3 gap-y-1 text-sm">
<div class="flex flex-col"> <span>Précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span>
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30">Maitrise de sorts</span> <span>Savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span>
<span>Sorts de précision: <span class="font-bold">{{ character.spellranks.precision }}</span></span> <span>Instinct: <span class="font-bold">{{ character.spellranks.instinct }}</span></span>
<span>Sorts de savoir: <span class="font-bold">{{ character.spellranks.knowledge }}</span></span> <span>Oeuvres: <span class="font-bold">{{ character.spellranks.arts }}</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> </div>
</div> </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"> <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> <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="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="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> <TabsTrigger value="notes" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Notes</TabsTrigger>
</TabsList> </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="flex flex-1 flex-col ps-8 gap-4 py-4">
<div class="grid grid-cols-3 gap-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-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> <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 } }" /> <MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col gap-2">
<span class="text-lg font-semibold">Réactions</span> <div class="flex flex-col">
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span> <span class="text-lg font-semibold">Réactions</span>
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" /> <span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
</div> <MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
<div class="flex flex-col"> </div>
<span class="text-lg font-semibold">Actions libre</span> <div class="flex flex-col">
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span> <span class="text-lg font-semibold">Actions libre</span>
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" /> <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> </div>
<div class="flex flex-col"> <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>
</div> </div>
</TabsContent> </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 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-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"> <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>
</div> </div>
</TabsContent> </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"> <div class="flex flex-1 flex-col ps-8 gap-4 py-8">
<MarkdownRenderer :content="character.notes" /> <MarkdownRenderer :content="character.notes" />
</div> </div>

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import type { CanvasContent, CanvasEdge, CanvasNode } from "~/types/canvas";
import { clamp, lerp } from "#shared/general.util"; import { clamp, lerp } from "#shared/general.util";
import { dom, icon, svg, text } from "#shared/dom.util"; import { dom, icon, svg, text } from "#shared/dom.util";
import render from "#shared/markdown.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 { History } from "#shared/history.util";
import { fakeA } from "#shared/proses"; import { fakeA } from "#shared/proses";
import { SpatialGrid } from "#shared/physics.util"; 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' }, [ 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: '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' }, [ 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')]), { 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'),
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' 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'),
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'
}),
]), ]),
]), this.transform, ]), 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() } } }, [ 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: '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' }, [ 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')]), { 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'),
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' 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'),
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'
}),
]), ]),
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [ 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')]), { 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'),
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' 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'),
}),
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'
}),
]), ]),
dom('div', { class: 'border border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10' }, [ 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')]), { 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'),
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' 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'),
}),
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'
}),
]), ]),
]), this.pattern, this.transform ]), this.pattern, this.transform
]); ]);

View File

@ -850,7 +850,7 @@
"5d7u2jvi4u0nnrzesderha3uo8kb3zjq" "5d7u2jvi4u0nnrzesderha3uo8kb3zjq"
], ],
"4": [ "4": [
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3" "4cm4mz365yupl9h8nfet7orbapeg2fzn"
], ],
"5": [ "5": [
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w" "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w"
@ -6082,7 +6082,7 @@
] ]
}, },
"3ugv3ym7bswjhz0drbx6v3932q7w3qsy": { "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", "id": "3ugv3ym7bswjhz0drbx6v3932q7w3qsy",
"effect": [ "effect": [
{ {
@ -6091,6 +6091,13 @@
"property": "mana", "property": "mana",
"operation": "add", "operation": "add",
"value": 3 "value": 3
},
{
"id": "mxshb0udl4zahch8l9v1rtpm1d5fv4au",
"category": "list",
"list": "spells",
"action": "add",
"item": "zltvtru98sm0ad9whiw5tty0gy4q2jur"
} }
] ]
}, },
@ -6193,9 +6200,17 @@
] ]
}, },
"iaaoqrn6kgvovzfpk4ygyd8yjwrxkml3": { "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", "id": "iaaoqrn6kgvovzfpk4ygyd8yjwrxkml3",
"effect": [] "effect": [
{
"id": "96xafpi1s4baffwht7gvfx7eqx3tzocq",
"category": "list",
"list": "spells",
"action": "add",
"item": "kd84l3gujh4evsyriti4g9sk1zwbxu8d"
}
]
}, },
"syj4q2o1qfh5vezi2d8bzgs9fn9ok273": { "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é.", "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": { "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", "id": "yn70y3tmxdo1w0zvm7at1i7uretcgkvk",
"effect": [ "effect": [
{ {
@ -6450,11 +6465,130 @@
"property": "modifier/curiosity", "property": "modifier/curiosity",
"operation": "add", "operation": "add",
"value": 1 "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": { "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", "id": "nqvexvg3ui6w2hqknwrw4pvf5axvu4go",
"effect": [ "effect": [
{ {
@ -6470,6 +6604,125 @@
"property": "modifier/curiosity", "property": "modifier/curiosity",
"operation": "add", "operation": "add",
"value": 1 "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": { "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", "id": "s5kidncgfzw85ffubl718lx2f68suhqf",
"effect": [ "effect": [
{ {
@ -8072,7 +8325,7 @@
] ]
}, },
"qf3eru17f8u3hysq56k246mlq7p2rbc9": { "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", "id": "qf3eru17f8u3hysq56k246mlq7p2rbc9",
"effect": [ "effect": [
{ {
@ -8358,9 +8611,17 @@
] ]
}, },
"sq43lzc8bdftrbbfwaq5l6nx1h5jx0eh": { "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", "id": "sq43lzc8bdftrbbfwaq5l6nx1h5jx0eh",
"effect": [] "effect": [
{
"id": "y7gnxzaf9puprd4ij56wk7bq8xjnt21d",
"category": "list",
"list": "spells",
"action": "add",
"item": "wj2rxkbw85zd9st8k2w3eezqc1naoy5g"
}
]
}, },
"c4nptbfb5uoyz98ovsqjxwlssgui9h9p": { "c4nptbfb5uoyz98ovsqjxwlssgui9h9p": {
"description": "Vous êtes capable d'utiliser les particularités magiques de votre Aspect sans vous transformer.", "description": "Vous êtes capable d'utiliser les particularités magiques de votre Aspect sans vous transformer.",
@ -9619,11 +9880,6 @@
"description": "suis", "description": "suis",
"effect": [] "effect": []
}, },
"8w4jthjrn3l8u4trmj46z6t6ab5rbgk3": {
"id": "8w4jthjrn3l8u4trmj46z6t6ab5rbgk3",
"description": "Nicolas",
"effect": []
},
"z9lux6nlhl8pjhcwst6bnhpn6cq6c77w": { "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w": {
"id": "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w", "id": "z9lux6nlhl8pjhcwst6bnhpn6cq6c77w",
"description": "Sarkozy", "description": "Sarkozy",
@ -9703,6 +9959,11 @@
"id": "qcp28eysi3l3n438v41kowisdpq4ht61", "id": "qcp28eysi3l3n438v41kowisdpq4ht61",
"description": "", "description": "",
"effect": [] "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 { fakeA } from "#shared/proses";
import { button, input, loading, numberpicker, select, toggle } from "#shared/components.util"; import { button, input, loading, numberpicker, select, toggle } from "#shared/components.util";
import { div, dom, icon, mergeClasses, text, type Class } from "#shared/dom.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 { clamp } from "#shared/general.util";
import markdownUtil from "#shared/markdown.util"; import markdownUtil from "#shared/markdown.util";
@ -34,7 +34,7 @@ export const defaultCharacter: Character = {
health: 0, health: 0,
mana: 0, mana: 0,
spells: [], spells: [],
equipment: [], items: [],
exhaustion: 0, exhaustion: 0,
sickness: [], sickness: [],
}, },
@ -198,8 +198,8 @@ export const CharacterValidation = z.object({
thumbnail: z.any(), thumbnail: z.any(),
}); });
type Property = { value: number | string | false, id: string, operation: "set" | "add" }; type Property = { value: number | string | false, id: string, operation: "set" | "add" | "min" };
type PropertySum = { list: Array<Property>, value: number, _dirty: boolean }; type PropertySum = { list: Array<Property>, min: number, value: number, _dirty: boolean };
export class CharacterCompiler export class CharacterCompiler
{ {
protected _character!: Character; protected _character!: Character;
@ -285,7 +285,7 @@ export class CharacterCompiler
return; return;
case "value": 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 }); this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
@ -318,7 +318,7 @@ export class CharacterCompiler
return; return;
case "value": 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); 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; sum += modifier.value;
else if(buffer.list[i]?.operation === 'set') else if(buffer.list[i]?.operation === 'set')
sum = modifier.value; sum = modifier.value;
else if(buffer.list[i]?.operation === 'min')
this._buffer[property]!.min = modifier.value;
} }
} }
else else
@ -383,6 +385,8 @@ export class CharacterCompiler
sum += buffer.list[i]!.value as number; sum += buffer.list[i]!.value as number;
else if(buffer.list[i]?.operation === 'set') else if(buffer.list[i]?.operation === 'set')
sum = buffer.list[i]!.value as number; 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); 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]!)) 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; this._buffer[property]!._dirty = false;
} }
} }
@ -423,7 +427,7 @@ export class CharacterBuilder extends CharacterCompiler
useRequestFetch()(`/api/character/${id}`).then(character => { useRequestFetch()(`/api/character/${id}`).then(character => {
if(character) if(character)
{ {
this._character = character; this.character = character;
document.title = `d[any] - Edition de ${character.name ?? 'nouveau personnage'}`; document.title = `d[any] - Edition de ${character.name ?? 'nouveau personnage'}`;
load.remove(); 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._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', [ 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("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 }), { 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") ]),
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"
}) ]),
]), ]),
this._content, this._content,
])); ]));
@ -683,6 +681,13 @@ export class PickableFeature
return this._content; return this._content;
} }
} }
class FeatureTable
{
constructor(table: Record<number, FeatureID[]>)
{
}
}
abstract class BuilderTab { abstract class BuilderTab {
protected _builder: CharacterBuilder; protected _builder: CharacterBuilder;
protected _content!: Array<Node | string>; protected _content!: Array<Node | string>;
@ -741,6 +746,8 @@ class PeoplePicker extends BuilderTab
]), ]),
button(text('Suivant'), () => this._builder.display(1), 'h-[35px] px-[15px]'), 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)]; ]), div('flex flex-1 gap-4 p-2 overflow-x-auto justify-center', this._options)];
this.update();
} }
override update() override update()
{ {
@ -798,6 +805,8 @@ class LevelPicker extends BuilderTab
]), ]),
button(text('Suivant'), () => this._builder.display(2), 'h-[35px] px-[15px]'), 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]))]; ]), div('flex flex-col flex-1 gap-4 mx-8 my-4', this._options.flatMap(e => [...e]))];
this.update();
} }
override update() override update()
{ {
@ -883,6 +892,8 @@ class TrainingPicker extends BuilderTab
]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])]; ]), div('flex flex-1 px-6 overflow-hidden max-w-full', [ this._statContainer ])];
this.switchTab(0); this.switchTab(0);
this.update();
} }
switchTab(tab: number) 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._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', [ 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 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); 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(); this._pointsInput.value = ((values.ability ?? 0) - abilities).toString();
return this._builder.character.abilities[e]; return this._builder.character.abilities[e];
}), popper(pushAndReturn(this._maxs, dom('span', { class: 'text-lg text-end cursor-pointer', text: '' })), { }}), tooltip(pushAndReturn(this._maxs, dom('span', { class: 'text-lg text-end cursor-pointer', text: '' })), pushAndReturn(this._tooltips, text('')), 'bottom-end')]),
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: "text-xl text-center font-bold", text: config.abilities[e].name }),
dom('span', { class: "absolute -bottom-px -left-px h-[3px] bg-accent-blue" }), 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]'), 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)]; ]), 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() override update()
{ {
@ -1002,13 +1009,11 @@ class AbilityPicker extends BuilderTab
this._pointsInput.value = ((values.ability ?? 0) - abilities).toString(); this._pointsInput.value = ((values.ability ?? 0) - abilities).toString();
ABILITIES.forEach((e, i) => { 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}%` }); 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}`; this._maxs[i]!.textContent = `/ ${max ?? 0}`;
return this._builder.character.abilities[e];
}) })
} }
static override validate(builder: CharacterBuilder): boolean static override validate(builder: CharacterBuilder): boolean
@ -1091,6 +1096,8 @@ class AspectPicker extends BuilderTab
]), ]),
button(text('Enregistrer'), () => this._builder.save(), 'h-[35px] px-[15px]'), 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)]; ]), 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() 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'}] }) 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) 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 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 { safeDestr as parse } from 'destr';
import { Canvas, CanvasEditor } from "#shared/canvas.util"; import { Canvas, CanvasEditor } from "#shared/canvas.util";
import render from "#shared/markdown.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 { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
import { loading } from "#shared/components.util"; import { loading } from "#shared/components.util";
import prose, { h1, h2 } from "#shared/proses"; 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 { TreeDOM, type Recursive } from '#shared/tree';
import { History } from '#shared/history.util'; import { History } from '#shared/history.util';
import { MarkdownEditor } from '#shared/editor.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)} }, [ 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` } }), 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 } }), 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' }), 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'),
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.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) => { }, (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) } }, [ 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 }), 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 } }), 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' }), 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'),
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.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>) private add(type: FileType, nextTo: Recursive<LocalContent>)
{ {
const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length; 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 }]); this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
} }
private remove(item: LocalContent & { element?: HTMLElement }) 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 { 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 { 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 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 renderMarkdown, { renderText } from "#shared/markdown.util";
import { Tree } from "#shared/tree"; import { Tree } from "#shared/tree";
import markdownUtil from "#shared/markdown.util"; import markdownUtil from "#shared/markdown.util";
@ -106,12 +106,12 @@ class PeopleEditor extends BuilderTab
const add = () => { const add = () => {
const people: RaceConfig = { const people: RaceConfig = {
id: getID(ID_SIZE), id: getID(),
name: '', name: '',
description: '', description: '',
options: LEVELS.map(e => { options: LEVELS.map(e => {
const feature: Feature = { const feature: Feature = {
id: getID(ID_SIZE), id: getID(),
description: '', description: '',
effect: [], effect: [],
} }
@ -143,7 +143,7 @@ class PeopleEditor extends BuilderTab
const context = contextmenu(e.clientX, e.clientY, [ 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: () => { 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(); context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] }; const _feature: Feature = { id: getID(), description: '', effect: [] };
config.features[_feature.id] = _feature; config.features[_feature.id] = _feature;
config.peoples[people]!.options[level]!.push(_feature.id); config.peoples[people]!.options[level]!.push(_feature.id);
element.parentElement?.appendChild(render(people, level, _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, [ 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: () => { 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(); context.close();
const _feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] }; const _feature: Feature = { id: getID(), description: '', effect: [] };
config.features[_feature.id] = _feature; config.features[_feature.id] = _feature;
config.training[stat][level].push(_feature.id); config.training[stat][level].push(_feature.id);
element.parentElement?.appendChild(render(stat, level, _feature.id)); element.parentElement?.appendChild(render(stat, level, _feature.id));
@ -326,7 +326,7 @@ class SpellEditor extends BuilderTab
} }
const add = () => { const add = () => {
config.spells.push({ config.spells.push({
id: getID(ID_SIZE), id: getID(),
name: '', name: '',
rank: 1, rank: 1,
type: 'precision', type: 'precision',
@ -404,7 +404,7 @@ export class FeatureEditor
div('flex flex-row justify-between', [ div('flex flex-row justify-between', [
dom('h3', { class: 'text-lg font-bold', text: 'Effets' }), dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => { 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'), }, 'p-1'), 'Ajouter', 'left'),
]), ]),
this._table, 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', [ 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('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => { 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'), () => { }, '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); this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id);
content.remove(); content.remove();
@ -484,7 +484,7 @@ export class FeatureEditor
tooltip(button(icon('radix-icons:update'), () => { 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); (buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
const newValueSelection = valueVariable(); const newValueSelection = valueVariable();
valueSelection?.parentElement?.replaceChild(newValueSelection, valueSelection); valueSelection.replaceWith(newValueSelection);
valueSelection = newValueSelection; valueSelection = newValueSelection;
summaryText.textContent = textFromEffect(buffer); 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'), }, '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) => { 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'; (buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove';
const element = redraw(); const element = redraw();
content?.parentElement?.replaceChild(element, content); content.replaceWith(element);
content = element; content = element;
}, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ]; }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-32' } }) ];
break; break;
case 'choice': case 'choice':
const add = () => { const 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); (buffer as Extract<FeatureItem, { category: 'choice' }>).options.push(option);
list.appendChild(render(option, true)); 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) => { 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 }; option = { id: option.id, ...e } as FeatureEffect & { text: string };
const element = render(option, true); const element = render(option, true);
_content?.parentElement?.replaceChild(element, _content); _content.replaceWith(element);
_content = 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'), () => { 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) => { 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; _buffer = { id: _buffer.id, ...e } as FeatureItem;
const element = redraw(); const element = redraw();
content?.parentElement?.replaceChild(element, content); content.replaceWith(element);
content = element; content = element;
} }), } }),
...top, ...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 (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: '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', value: [
{ text: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } }, { 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 } }, { 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 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 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 } }, { 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 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 force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 },
{ text: 'Modifieur de dextérité', category: 'value', property: 'modifier/dexterity', 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 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 charisme', category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 },
{ text: 'Modifieur de psyché', egory: 'value', property: 'modifier/psyche', 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: 'Jet de résistance', value: [
{ text: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } }, { 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: '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: '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 } }, { 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: '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: 'Force', category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 },
{ text: 'Dextérité', category: 'value', property: 'bonus/defense/dexterity', 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: 'Curiosité', category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 },
{ text: 'Charisme', category: 'value', property: 'bonus/defense/charisma', 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 } { 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: '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: [ { text: 'Rang', value: [
@ -760,12 +761,14 @@ function textFromEffect(effect: Partial<FeatureItem>): string
{ {
case 'resistance': 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).' }); 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': 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).` }); 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': 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': 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).` }); 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; default: break;

View File

@ -19,11 +19,11 @@ export interface FollowerProperties extends FloatingProperties
} }
export interface PopperProperties extends FloatingProperties export interface PopperProperties extends FloatingProperties
{ {
content?: NodeChildren; content?: NodeChildren | (() => NodeChildren);
delay?: number; delay?: number;
onShow?: (element: HTMLDivElement) => boolean | void; onShow?: () => boolean | void;
onHide?: (element: HTMLDivElement) => boolean | void; onHide?: () => boolean | void;
} }
export interface ModalProperties export interface ModalProperties
@ -43,7 +43,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
{ {
let shown = false, timeout: Timer; 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 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'; const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
function update() function update()
@ -53,7 +53,6 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
strategy: 'fixed', strategy: 'fixed',
middleware: [ middleware: [
properties?.offset ? FloatingUI.offset(properties?.offset) : undefined, properties?.offset ? FloatingUI.offset(properties?.offset) : undefined,
FloatingUI.hide({ rootBoundary: rect, strategy: "escaped" }),
FloatingUI.hide({ rootBoundary: rect }), FloatingUI.hide({ rootBoundary: rect }),
FloatingUI.shift({ rootBoundary: rect }), FloatingUI.shift({ rootBoundary: rect }),
FloatingUI.flip({ rootBoundary: rect }), FloatingUI.flip({ rootBoundary: rect }),
@ -109,8 +108,17 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
let stop: () => void | undefined; let stop: () => void | undefined;
function show() 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); clearTimeout(timeout);
timeout = setTimeout(() => { timeout = setTimeout(() => {
@ -137,7 +145,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
function hide() function hide()
{ {
if(!properties?.onHide || properties?.onHide(content) !== false) if(!properties?.onHide || properties?.onHide() !== false)
{ {
clearTimeout(timeout); clearTimeout(timeout);
@ -274,13 +282,13 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
}, },
}, content, properties); }, 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, { return popper(container, {
arrow: true, arrow: true,
offset: 8, offset: 8,
delay: delay, delay: delay,
content: [ text(txt) ], content: [ typeof txt === 'string' ? text(txt) : txt ],
placement: placement, 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" 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 export function unifySlug(slug: string | string[]): string
{ {
return (Array.isArray(slug) ? slug.join('/') : slug); 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)); id.push((36 * Math.random() | 0).toString(36));
return id.join(""); 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'; import characterConfig from '#shared/character-config.json';
const config = characterConfig as CharacterConfig; 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; 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 { heading } from "hast-util-heading";
import { headingRank } from "hast-util-heading-rank"; import { headingRank } from "hast-util-heading-rank";
import { parseId } from "#shared/general.util"; 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 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 export default function(content: string, filter?: string, properties?: MDProperties): HTMLElement
{ {
const load = loading('normal'); return async('large', useMarkdown().parse(content).then(data => {
if(filter)
queueMicrotask(() => { {
useMarkdown().parse(content).then(data => { const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
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);
load.parentElement?.replaceChild(el, load); if(start !== -1)
}); {
}) let end = start;
const rank = headingRank(data.children[start])!;
return load; 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 { parseURL } from 'ufo';
import render from "#shared/markdown.util"; import render from "#shared/markdown.util";
import { popper } from "#shared/floating.util"; import { popper } from "#shared/floating.util";
import { Canvas } from "#shared/canvas.util"; import { Canvas } from "#shared/canvas.util";
import { Content, iconByType, type LocalContent } from "#shared/content.util"; import { Content, iconByType, type LocalContent } from "#shared/content.util";
import { parsePath, unifySlug } from "#shared/general.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; 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); 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: { const el = dom('a', { class: 'text-accent-blue inline-flex items-center', attributes: { href: nav.href }, listeners: {
'click': (e) => { 'click': (e) => {
e.preventDefault(); e.preventDefault();
@ -43,26 +41,22 @@ export const a: Prose = {
cover: "height", cover: "height",
placement: 'bottom-start', 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]', 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")], content: () => {
viewport: document.getElementById('mainContainer') ?? undefined, return [async('large', Content.getContent(overview.id).then((_content) => {
onShow(content: HTMLDivElement) { if(_content?.type === 'markdown')
if(!rendered) {
{ 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' });
Content.getContent(overview.id).then((_content) => { }
if(_content?.type === 'markdown') if(_content?.type === 'canvas')
{ {
content.replaceChild(render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'py-4 px-6' }), content.children[0]!); const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
} queueMicrotask(() => canvas.mount());
if(_content?.type === 'canvas') return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]);
{ }
const canvas = new Canvas((_content as LocalContent<'canvas'>).content); return div('');
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!); }))];
canvas.mount();
}
});
rendered = true;
}
}, },
viewport: document.getElementById('mainContainer') ?? undefined
}); });
} }
@ -95,24 +89,24 @@ export const fakeA: Prose = {
cover: "height", cover: "height",
placement: 'bottom-start', 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 z-[45]',
content: [loading("large")], content: () => {
onShow(content: HTMLDivElement) { return [async('large', Content.getContent(overview.id).then((_content) => {
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
return false;
content.replaceChild(loading("large"), content.children[0]!);
Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown') 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') if(_content?.type === 'canvas')
{ {
const canvas = new Canvas((_content as LocalContent<'canvas'>).content); const canvas = new Canvas((_content as LocalContent<'canvas'>).content);
content.replaceChild(dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]), content.children[0]!); queueMicrotask(() => canvas.mount());
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 Alignment = typeof ALIGNMENTS[number];
export type FeatureID = string; export type FeatureID = string;
export type TextID = string; export type i18nID = string;
export type Resistance = string; export type Resistance = string;
export type Dice = `${number}d${4 | 6 | 8 | 10 | 12 | 20}`;
export type Character = { export type Character = {
id: number; id: number;
@ -41,7 +39,15 @@ export type CharacterVariables = {
sickness: Array<{ id: string, state: number | true }>; sickness: Array<{ id: string, state: number | true }>;
spells: string[]; //Spell ID 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 = { export type CharacterConfig = {
peoples: Record<string, RaceConfig>; peoples: Record<string, RaceConfig>;
@ -51,35 +57,44 @@ export type CharacterConfig = {
spells: SpellConfig[]; spells: SpellConfig[];
aspects: AspectConfig[]; aspects: AspectConfig[];
features: Record<FeatureID, Feature>; features: Record<FeatureID, Feature>;
enchantments: Record<string, { name: string, effect: FeatureEffect[] }>; //TODO enchantments: Record<string, { name: string, effect: FeatureEffect[], power: number }>; //TODO
items: Record<string, ItemConfig & { enchantments: string[] }>; items: Record<string, ItemConfig>;
lists: Record<string, { id: string, name: string, [key: string]: any }[]>; 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 = { type ArmorConfig = {
category: 'armor'; category: 'armor';
name: string; //TODO -> TextID name: string; //TODO -> TextID
description: TextID; description: i18nID;
life: number; health: number;
absorb: number; absorb: { static: number, percent: number };
}; };
type WeaponConfig = { type WeaponConfig = {
category: 'armor'; category: 'weapon';
name: string; //TODO -> TextID name: string; //TODO -> TextID
description: TextID; description: i18nID;
damage: Dice; damage: string; //Dice formula
}; };
type WondrousConfig = { type WondrousConfig = {
category: 'armor'; category: 'wondrous';
name: string; //TODO -> TextID name: string; //TODO -> TextID
description: TextID; description: i18nID;
effect: FeatureEffect[]; effect: FeatureEffect[];
}; };
type MundaneConfig = { type MundaneConfig = {
category: 'armor'; category: 'mundane';
name: string; //TODO -> TextID name: string; //TODO -> TextID
description: TextID; description: i18nID;
}; };
export type SpellConfig = { export type SpellConfig = {
id: string; id: string;
@ -120,7 +135,7 @@ export type AspectConfig = {
export type FeatureEffect = { export type FeatureEffect = {
id: FeatureID; id: FeatureID;
category: "value"; category: "value";
operation: "add" | "set"; operation: "add" | "set" | "min";
property: string; property: string;
value: number | `modifier/${MainStat}` | false; value: number | `modifier/${MainStat}` | false;
} | { } | {
@ -140,10 +155,10 @@ export type FeatureItem = FeatureEffect | {
exclusive: boolean; //Disallow to pick the same option twice exclusive: boolean; //Disallow to pick the same option twice
}; };
options: Array<FeatureEffect & { text: string }>; options: Array<FeatureEffect & { text: string }>;
} };
export type Feature = { export type Feature = {
id: FeatureID; id: FeatureID;
description: string; description: i18nID;
effect: FeatureItem[]; effect: FeatureItem[];
}; };