This commit is contained in:
Peaceultime 2025-09-30 21:50:59 +02:00
commit 2b39f26722
20 changed files with 1162 additions and 855 deletions

View File

@ -4,7 +4,7 @@
<script setup lang="ts">
import { parseURL } from 'ufo';
import proses, { fakeA } from '#shared/proses';
import proses, { preview } from '#shared/proses';
import { text } from '#shared/dom.util';
const { href, label } = defineProps<{
@ -16,7 +16,7 @@ const container = useTemplateRef('container');
onMounted(() => {
queueMicrotask(() => {
container.value && container.value.appendChild(proses('a', fakeA, [ text(label) ], { href }) as HTMLElement);
container.value && container.value.appendChild(proses('a', preview, [ text(label) ], { href }) as HTMLElement);
});
});
</script>

View File

@ -55,7 +55,7 @@ export const characterTable = table("character", {
owner: int().notNull().references(() => usersTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
people: text().notNull(),
level: int().notNull().default(1),
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"equipment": [],"exhaustion": 0,"sickness": []}'),
variables: text({ mode: 'json' }).notNull().default('{"health": 0,"mana": 0,"spells": [],"equipment": [],"exhaustion": 0,"sickness": [],"poisons": []}'),
aspect: int(),
notes: text(),

View File

@ -2,25 +2,16 @@
import characterConfig from '#shared/character-config.json';
import { Icon } from '@iconify/vue/dist/iconify.js';
import PreviewA from '~/components/prose/PreviewA.vue';
import { clamp } from '#shared/general.util';
import { clamp, unifySlug } from '#shared/general.util';
import type { CompiledCharacter, SpellConfig } from '~/types/character';
import type { CharacterConfig } from '~/types/character';
import { abilityTexts, CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '#shared/character.util';
import { abilityTexts, CharacterCompiler, CharacterSheet, defaultCharacter, elementTexts, spellTypeTexts } from '#shared/character.util';
import { getText } from '#shared/i18n';
import { fakeA } from '#shared/proses';
import { preview } from '#shared/proses';
import { div, dom, icon, text } from '#shared/dom.util';
import markdown from '#shared/markdown.util';
import { button, foldable } from '#shared/components.util';
import { fullblocker, tooltip } from '~/shared/floating.util';
const config = characterConfig as CharacterConfig;
const id = useRouter().currentRoute.value.params.id;
const { user } = useUserSession();
const { data, status, error } = await useFetch(`/api/character/${id}`);
const compiler = new CharacterCompiler(data.value ?? defaultCharacter);
const character = ref<CompiledCharacter>(compiler.compiled);
/*
text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red
text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue
@ -33,76 +24,26 @@ text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yel
text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple
*/
function openSpellPanel() {
const availableSpells = Object.values(config.spells).filter(spell => {
if (spell.rank === 4) return false;
if (character.value.spellranks[spell.type] < spell.rank) return false;
return true;
});
const config = characterConfig as CharacterConfig;
const textAmount = text(character.value.variables.spells.length.toString()), textMax = text(character.value.spellslots.toString());
const container = div("border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
div("flex flex-row justify-between items-center mb-4", [
dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }),
div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ textAmount, text(' / '), textMax, text(' sorts maitrisés') ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left") ])
]),
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', availableSpells.map(spell => {
let state = character.value.lists.spells?.includes(spell.id) ? 'given' : character.value.variables.spells.includes(spell.id) ? 'choosen' : 'empty';
const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => {
if(state === 'choosen')
const id = useRouter().currentRoute.value.params.id ? unifySlug(useRouter().currentRoute.value.params.id!) : undefined;
const { user } = useUserSession();
const container = useTemplateRef('container');
onMounted(() => {
queueMicrotask(() => {
if(container.value && id)
{
compiler.variable('spells', character.value.variables.spells.filter(e => e !== spell.id));
state = 'empty';
}
else if(state === 'empty')
{
compiler.variable('spells', [...character.value.variables.spells, spell.id]);
state = 'choosen';
}
character.value = compiler.compiled;
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
textAmount.textContent = character.value.variables.spells.length.toString();
}, "px-2 py-1 text-sm font-normal");
toggleButton.disabled = state === 'given';
return foldable(() => [
markdown(spell.effect),
], [ div("flex flex-row justify-between gap-2", [
dom("span", { class: "text-lg font-bold", text: spell.name }),
div("flex flex-row items-center gap-6", [
div("flex flex-row text-sm gap-2",
spell.elements.map(el =>
dom("span", {
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[el].class],
text: elementTexts[el].text
})
)
),
div("flex flex-row text-sm gap-1", [
...(spell.rank !== 4 ? [
dom("span", { text: `Rang ${spell.rank}` }),
text("/"),
dom("span", { text: spellTypeTexts[spell.type] }),
text("/")
] : []),
dom("span", { text: `${spell.cost} mana` }),
text("/"),
dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` })
]),
toggleButton,
]),
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
}))
]);
const blocker = fullblocker([ container ], { closeWhenOutside: true });
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
const character = new CharacterSheet(id, user);
container.value.appendChild(character.container);
}
});
});
</script>
<template>
<div v-if="status === 'pending'">
<div ref="container"></div>
<!-- <div v-if="status === 'pending'">
<Head>
<Title>d[any] - Chargement ...</Title>
</Head>
@ -125,9 +66,9 @@ function openSpellPanel() {
<span>{{ config.peoples[character.race]?.name ?? 'Peuple inconnu' }}</span>
</div>
</div>
<div class="flex flex-col lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4">
<span class="flex flex-row items-center gap-2">PV: {{ character.health - character.variables.health }}/{{ character.health }}</span>
<span class="flex flex-row items-center gap-2">Mana: {{ character.mana - character.variables.mana }}/{{ character.mana }}</span>
<div class="flex flex-row lg:border-l border-light-30 dark:border-dark-30 py-4 ps-4 gap-8">
<span class="flex flex-row items-center gap-2 text-3xl font-light">PV: <span class="font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35">{{ character.health - character.variables.health }}</span>/ {{ character.health }}</span>
<span class="flex flex-row items-center gap-2 text-3xl font-light">Mana: <span class="font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35">{{ character.mana - character.variables.mana }}</span>/ {{ character.mana }}</span>
</div>
</div>
<div class="self-center">
@ -135,8 +76,8 @@ function openSpellPanel() {
</div>
</div>
<div class="flex flex-1 flex-col justify-center gap-4 *:py-2">
<div class="grid 2xl:grid-cols-10 grid-cols-1 gap-4 items-center border-b border-light-30 dark:border-dark-30 me-4 pe-4">
<div class="flex relative justify-between ps-4 gap-2 2xl:col-span-6">
<div class="flex flex-row gap-4 items-center border-b border-light-30 dark:border-dark-30 me-4 pe-4 divide-x divide-light-30 dark:divide-dark-30">
<div class="flex relative justify-between ps-4 gap-2">
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.strength }}</span><span class="text-sm 2xl:text-base">Force</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.dexterity }}</span><span class="text-sm 2xl:text-base">Dextérité</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.constitution }}</span><span class="text-sm 2xl:text-base">Constitution</span></div>
@ -145,9 +86,11 @@ function openSpellPanel() {
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.charisma }}</span><span class="text-sm 2xl:text-base">Charisme</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">+{{ character.modifier.psyche }}</span><span class="text-sm 2xl:text-base">Psyché</span></div>
</div>
<div class="flex flex-1 relative 2xl:border-l border-light-30 dark:border-dark-30 ps-4 2xl:col-span-4 flex-row items-center justify-between">
<div class="flex flex-1 relative ps-4 flex-row items-center justify-between">
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">+{{ character.initiative }}</span><span>Initiative</span></div>
<div class="flex flex-col px-2 items-center"><span class="text-2xl font-bold">{{ character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }}</span><span>Course</span></div>
</div>
<div class="flex flex-1 relative ps-4 flex-row items-center justify-between">
<Icon icon="ph:shield-checkered" class="w-8 h-8" />
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Passive</span></div>
<div class="flex flex-col items-center"><span class="2xl:text-2xl text-xl font-bold">{{ clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap) }}</span><span class="text-sm 2xl:text-base">Blocage</span></div>
@ -205,24 +148,24 @@ function openSpellPanel() {
<div class="flex flex-col col-span-2">
<span class="text-lg font-semibold">Actions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Attaquer - Saisir - Faire chuter - Déplacer - Courir - Pas de coté - Lancer un sort - S'interposer - Se transformer - Utiliser un objet - Anticiper une action - Improviser</span>
<MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
<MarkdownRenderer :content="character.lists.action?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
</div>
<div class="flex flex-col gap-2">
<div class="flex flex-col">
<span class="text-lg font-semibold">Réactions</span>
<span class="text-sm text-light-70 dark:text-dark-70">Parade - Esquive - Saisir une opportunité - Prendre en tenaille - Intercepter - Désarmer</span>
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
<MarkdownRenderer :content="character.lists.reaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Actions libre</span>
<span class="text-sm text-light-70 dark:text-dark-70">Analyser une situation - Communiquer</span>
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: fakeA } }" />
<MarkdownRenderer :content="character.lists.freeaction?.map(e => getText(e))?.join('\n')" :properties="{ tags: { a: preview } }" />
</div>
</div>
</div>
<div class="flex flex-col">
<span class="text-lg font-semibold">Aptitudes</span>
<MarkdownRenderer :content="character.lists.passive?.map(e => getText(e))?.map(e => `> ${e}`).join('\n\n')" :properties="{ tags: { a: fakeA } }" />
<MarkdownRenderer :content="character.lists.passive?.map(e => getText(e))?.map(e => `> ${e}`).join('\n\n')" :properties="{ tags: { a: preview } }" />
</div>
</div>
</TabsContent>
@ -251,9 +194,7 @@ function openSpellPanel() {
</div>
</TabsContent>
<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">
@ -269,5 +210,5 @@ function openSpellPanel() {
<Title>d[any] - Erreur</Title>
</Head>
<div>Erreur de chargement</div>
</div>
</div> -->
</template>

View File

@ -1,5 +1,8 @@
<script setup lang="ts">
import characterConfig from '#shared/character-config.json';
import type { CharacterConfig } from '~/types/character';
const { data: characters, error, status } = await useFetch(`/api/character`, { params: { visibility: "public" } });
const config = characterConfig as CharacterConfig;
</script>
<template>
@ -11,13 +14,22 @@ const { data: characters, error, status } = await useFetch(`/api/character`, { p
<Loading size="large" />
</div>
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters">
<Avatar size="large" icon="radix-icons:person" src="" />
<div class="flex flex-1 flex-shrink flex-col truncate">
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink>
<span class="text-sm truncate">Niveau {{ character.level }}</span>
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
<NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="group bg-light-10 dark:bg-dark-10 p-2 flex flex-col gap-2">
<div class="flex flex-row gap-8 ps-4 items-center">
<div class="flex flex-1 flex-col gap-2 justify-center">
<span class="text-lg font-bold group-hover:text-accent-blue">{{ character.name }}</span>
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
<div class="flex flex-row flex-1 items-stretch gap-4">
<span class="text-sm">Niveau {{ character.level }}</span>
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
</div>
</div>
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
</div>
</NuxtLink>
</div>
</div>
<div v-else>
<span>Erreur de chargement</span>

View File

@ -4,7 +4,11 @@ declare module 'nitropack'
{
interface TaskPayload
{
type: string
type: string;
}
interface TaskResult<RT = unknown>
{
error?: Error | string;
}
}
@ -17,7 +21,7 @@ export default defineEventHandler(async (e) => {
return;
}
const id = getRouterParam(e, 'id');
const payload: Record<string, any> = await readBody(e);
const body: Record<string, any> = await readBody(e);
if(!id)
{
@ -25,8 +29,11 @@ export default defineEventHandler(async (e) => {
return;
}
payload.type = id;
payload.data = JSON.parse(payload.data);
body.data = JSON.parse(body.data);
const payload = {
type: id,
data: body,
}
const result = await runTask(id, {
payload: payload
@ -36,7 +43,7 @@ export default defineEventHandler(async (e) => {
{
setResponseStatus(e, 500);
if(result.error && (result.error as Error).message)
if(result.error && result.error.message)
throw result.error;
else if(result.error)
throw new Error(result.error);

View File

@ -25,7 +25,7 @@ export default defineEventHandler(async (e) => {
return;
}
where = (character, { eq, and }) => and(eq(character.owner, session.user!.id), eq(character.visibility, "private"));
where = (character, { eq, and }) => and(eq(character.owner, session.user!.id));
}
else if(visibility === 'public')
{

View File

@ -37,12 +37,12 @@ export default defineEventHandler(async (e) => {
thumbnail: body.data.thumbnail,
}).returning({ id: characterTable.id }).get().id;
if(Object.keys(body.data.leveling).length > 0) tx.insert(characterLevelingTable).values(Object.entries(body.data.leveling).map(e => ({ character: id, level: parseInt(e[0], 10), choice: e[1]! }))).run();
if(Object.keys(body.data.leveling).length > 0) tx.insert(characterLevelingTable).values(Object.entries(body.data.leveling).filter(e => e[1] !== undefined).map(e => ({ character: id, level: parseInt(e[0], 10), choice: e[1]! }))).run();
const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0], 10), choice: _e[1]! })));
const training = Object.entries(body.data.training).flatMap(e => Object.entries(e[1]).filter(e => e[1] !== undefined).map(_e => ({ character: id, stat: e[0] as MainStat, level: parseInt(_e[0], 10), choice: _e[1]! })));
if(training.length > 0) tx.insert(characterTrainingTable).values(training).run();
const abilities = Object.entries(body.data.abilities).map(e => ({ character: id, ability: e[0] as Ability, value: e[1] }));
const abilities = Object.entries(body.data.abilities).filter(e => e[1] !== undefined).map(e => ({ character: id, ability: e[0] as Ability, value: e[1] }));
if(abilities.length > 0) tx.insert(characterAbilitiesTable).values(abilities).run();
return id;

View File

@ -1,35 +0,0 @@
import { and, eq, sql } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { CharacterVariables } from '~/types/character';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
if(!id)
{
setResponseStatus(e, 400);
return;
}
const session = await getUserSession(e);
if(!session.user)
{
setResponseStatus(e, 401);
return;
}
const db = useDatabase();
const character = db.select({
health: characterTable.health,
mana: characterTable.mana,
}).from(characterTable).where(and(eq(characterTable.id, parseInt(id, 10)), eq(characterTable.owner, session.user.id))).get();
if(character !== undefined)
{
return character as CharacterVariables;
}
setResponseStatus(e, 404);
return;
});

View File

@ -1,7 +1,8 @@
import { eq } from 'drizzle-orm';
import useDatabase from '~/composables/useDatabase';
import { characterTable } from '~/db/schema';
import type { CharacterValues } from '~/types/character';
import { CharacterVariablesValidation } from '~/shared/character.util';
import type { CharacterVariables } from '~/types/character';
export default defineEventHandler(async (e) => {
const id = getRouterParam(e, "id");
@ -11,11 +12,12 @@ export default defineEventHandler(async (e) => {
return;
}
const body = await readBody(e) as CharacterValues;
if(!body)
const body = await readValidatedBody(e, CharacterVariablesValidation.safeParse);
if(!body.success)
{
console.error(body.error);
setResponseStatus(e, 400);
return;
throw body.error;
}
const db = useDatabase();
@ -35,8 +37,7 @@ export default defineEventHandler(async (e) => {
}
db.update(characterTable).set({
health: body.health,
mana: body.mana,
variables: body.data
}).where(eq(characterTable.id, parseInt(id, 10))).run();
setResponseStatus(e, 200);

View File

@ -1,9 +1,9 @@
<template>
<div style='margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;'>
<div style="margin-left: auto; margin-right: auto; text-align: center;">
<a href="https://d-any.com">
<a style="display: inline-block;" href="https://d-any.com">
<img src="https://d-any.com/logo.light.png" alt="Logo" title="d[any] logo" width="64" height="64" style="display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;" />
<span style="margin-inline-end: 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
<span style="margin-inline-end: 1rem; font-size: 1.5rem; color: black; text-decoration: none; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
</a>
</div>
<div style="padding: 1rem;">
@ -11,6 +11,6 @@
</div>
</div>
<div style="background-color: #171717;">
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime - d[any] - 2024</p>
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime / d[any] - 2024 / 2025</p>
</div>
</template>

View File

@ -17,10 +17,9 @@ export const templates: Record<string, { component: any, subject: string }> = {
import type Mail from 'nodemailer/lib/mailer';
interface MailPayload
{
type: 'mail'
to: string[]
template: string
data: Record<string, any>
to: string[];
template: string;
data: Record<string, any>;
}
const transport = nodemailer.createTransport({
@ -55,26 +54,33 @@ if(process.env.NODE_ENV === 'production')
});
}
export default async function(e: TaskEvent) {
export default defineTask({
meta: {
name: 'mail',
description: ''
},
run: async ({ payload, context }) => {
try {
if(e.payload.type !== 'mail')
if(payload.type !== 'mail')
{
throw new Error(`Données inconnues`);
}
const payload = e.payload as MailPayload;
const template = templates[payload.template];
const mailPayload = payload.data as MailPayload;
const template = templates[mailPayload.template];
console.log(mailPayload);
if(!template)
{
throw new Error(`Modèle de mail ${payload.template} inconnu`);
throw new Error(`Modèle de mail ${mailPayload.template} inconnu`);
}
console.time('Generating HTML');
const mail: Mail.Options = {
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
to: payload.to,
html: await render(template.component, payload.data),
to: mailPayload.to,
html: await render(template.component, mailPayload.data),
subject: template.subject,
textEncoding: 'quoted-printable',
};
@ -100,6 +106,7 @@ export default async function(e: TaskEvent) {
return { result: false, error: e };
}
}
})
async function render(component: any, data: Record<string, any>): Promise<string>
{

View File

@ -4,7 +4,7 @@ import { dom, icon, svg, text } from "#shared/dom.util";
import render from "#shared/markdown.util";
import { popper, tooltip } from "#shared/floating.util";
import { History } from "#shared/history.util";
import { fakeA } from "#shared/proses";
import { preview } from "#shared/proses";
import { SpatialGrid } from "#shared/physics.util";
import type { CanvasPreferences } from "~/types/general";
@ -189,7 +189,7 @@ export class NodeEditable extends Node
this.nodeDom = dom('div', { class: ['absolute group', {'-z-10': this.properties.type === 'group', 'z-10': this.properties.type !== 'group'}], style: { transform: `translate(${this.properties.x}px, ${this.properties.y}px)`, width: `${this.properties.width}px`, height: `${this.properties.height}px`, '--canvas-color': this.properties.color?.hex } }, [
dom('div', { class: ['outline-0 transition-[outline-width] border-2 bg-light-20 dark:bg-dark-20 w-full h-full group-hover:outline-4', style.border, style.outline] }, [
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'text' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: fakeA } })]) : undefined])
dom('div', { class: ['w-full h-full py-2 px-4 flex !bg-opacity-[0.07] overflow-auto', style.bg], listeners: this.properties.type === 'text' ? { mouseenter: e => this.dispatchEvent(new CustomEvent('focus', { detail: this })), click: e => this.dispatchEvent(new CustomEvent('select', { detail: this })), dblclick: e => this.dispatchEvent(new CustomEvent('edit', { detail: this })) } : undefined }, [this.properties.text ? dom('div', { class: 'flex items-center' }, [render(this.properties.text, undefined, { tags: { a: preview } })]) : undefined])
])
]);

View File

@ -558,12 +558,18 @@
}
},
"lists": {
"sickness": [
{
"id": "",
"sickness": {
"id": "sickness",
"config": {
},
"values": {
"rotted": {
"id": "rotted",
"name": "Pourriture mortelle"
}
]
}
}
},
"peoples": {
"e662m19q590kn4dowvssowi1qf8ia7sk": {
@ -4876,29 +4882,41 @@
"category": "choice",
"text": "Vous avez un bonus de +1 aux jets de résistance de ",
"options": [
{
"text": "Force",
"effects": [
{
"id": "sx1vca2kzustsjatvslbjl68guv45m0b",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
"property": "bonus/defense/strength",
"value": 1
}
]
},
{
"text": "Dextérité",
"effects": [
{
"id": "41mflh7px0otbj169q8mr5btc8qie18g",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
"property": "bonus/defense/dexterity",
"value": 1
}
]
},
{
"text": "Constitution",
"effects": [
{
"id": "55vp7dpdto073hrqg11aemyxxo9skg0q",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
"property": "bonus/defense/constitution",
"value": 1
}
]
}
]
},
@ -5073,26 +5091,36 @@
"exclusive": true
},
"options": [
{
"text": "Force",
"effects": [
{
"id": "sx1vca2kzustsjatvslbjl68guv45m0b",
"category": "value",
"text": "Force",
"operation": "add",
"property": "bonus/defense/strength",
"value": 1
}
]
},
{
"text": "Dextérité",
"effects": [
{
"id": "41mflh7px0otbj169q8mr5btc8qie18g",
"category": "value",
"text": "Dextérité",
"operation": "add",
"property": "bonus/defense/dexterity",
"value": 1
}
]
},
{
"text": "Constitution",
"effects": [
{
"id": "55vp7dpdto073hrqg11aemyxxo9skg0q",
"category": "value",
"text": "Constitution",
"operation": "add",
"property": "bonus/defense/constitution",
"value": 1
@ -5100,6 +5128,8 @@
]
}
]
}
]
},
"rtrvkfz12xac62ziqi586iinzywmafdo": {
"description": "Une fois par combat, vous pouvez passer votre tour pour perdre un point de [[3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]].",
@ -5242,26 +5272,36 @@
"category": "choice",
"text": "Une fois par [[3. Glossaire#Long repos|long repos]], vous pouvez réussir votre [[3. Résistance aux chocs#Le jet de résistance|jet de résistance]] de cette statistique sans lancer de dés.",
"options": [
{
"text": "Force",
"effects": [
{
"id": "sx1vca2kzustsjatvslbjl68guv45m0b",
"category": "value",
"text": "Force",
"operation": "add",
"property": "bonus/defense/strength",
"value": 1
}
]
},
{
"text": "Dextérité",
"effects": [
{
"id": "41mflh7px0otbj169q8mr5btc8qie18g",
"category": "value",
"text": "Dextérité",
"operation": "add",
"property": "bonus/defense/dexterity",
"value": 1
}
]
},
{
"text": "Constitution",
"effects": [
{
"id": "55vp7dpdto073hrqg11aemyxxo9skg0q",
"category": "value",
"text": "Constitution",
"operation": "add",
"property": "bonus/defense/constitution",
"value": 1
@ -5269,6 +5309,8 @@
]
}
]
}
]
},
"g5phzahr4ss3fbacb22ktz1qby3ou7n9": {
"description": "Vous pouvez subir un point de [[3. Fatigue et repos#Fatigue temporaire|fatigue temporaire]] pour gagner un point d'action durant ce tour.",
@ -5768,87 +5810,14 @@
]
},
"dxlevxrlacugpj4jvdjs5bxecraoxbnp": {
"description": "Choisissez une [[1. Magie#Les éléments|classe élémentaire]]. Lorsque vous voyez un sort de cet élément être lancé à 12 cases de vous, vous pouvez [[2. Actions en combat#Saisir une opportunité|saisir l'opportunité]] pour dépenser l'intégralité du coût en mana à la place du lanceur. *Vous appliquez le coût en mana du lanceur d'origine.*",
"description": "Choisissez une [[1. Magie#Les éléments|classe élémentaire]]. Lorsque vous voyez un sort de cet élément être lancé à 12 cases de vous, vous pouvez [[2. Actions en combat#Saisir une opportunité|saisir l'opportunité]] pour dépenser l'intégralité du coût en mana à la place du lanceur. *Vous appliquez le coût en mana du lanceur d'origine.* #todo",
"id": "dxlevxrlacugpj4jvdjs5bxecraoxbnp",
"effect": [
{
"id": "ix2y02up7p04hzv1bhyer0cnl5eedjj3",
"category": "choice",
"text": "Lorsque vous voyez un sort de cet élément être lancé à 12 cases de vous, vous pouvez saisir l'opportunité pour dépenser l'intégralité du coût en mana à la place du lanceur.",
"options": [
{
"id": "hrz76l4hu874uyz1to2yh2cej71hyk67",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "yp8ito93tx24f8htq4fdougeyjf31y85",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "o8v66orebhiemsnn4rkffs705sqjml0f",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "w7hvncya5m7xbi6igda7r9tlbdkuyg1t",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "ubtwd3sl3y27heps9ev99w8piur2l0zv",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "7kwst41c2eecgop178rfcszidz2t44pf",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "47aqw1fy16dszuircpp1vxco2twq9gah",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "xz3bfma0nh3q7csn83kt2ftij6irpxsg",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "vtowf22lk7gl0rgpjfkss8uh50peaj3o",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
}
]
"options": []
}
]
},
@ -6993,54 +6962,82 @@
"options": [
{
"text": "Force",
"effects": [
{
"category": "value",
"property": "modifier/strength",
"operation": "add",
"value": 1
}
]
},
{
"text": "Dextérité",
"effects": [
{
"category": "value",
"property": "modifier/dexterity",
"operation": "add",
"value": 1
}
]
},
{
"text": "Constitution",
"effects": [
{
"category": "value",
"property": "modifier/constitution",
"operation": "add",
"value": 1
}
]
},
{
"text": "Intelligence",
"effects": [
{
"category": "value",
"property": "modifier/intelligence",
"operation": "add",
"value": 1
}
]
},
{
"text": "Curiosité",
"effects": [
{
"category": "value",
"property": "modifier/curiosity",
"operation": "add",
"value": 1
}
]
},
{
"text": "Charisme",
"effects": [
{
"category": "value",
"property": "modifier/charisma",
"operation": "add",
"value": 1
}
]
},
{
"text": "Psyché",
"effects": [
{
"egory": "value",
"property": "modifier/psyche",
"operation": "add",
"value": 1
}
]
}
]
},
{
"id": "o2ed50qccdwbqbjyo1t7rwjpr82jbk9o",
@ -7144,151 +7141,14 @@
]
},
"7ii1ig85j7a1gacorzkn6oyjdt3w6jzh": {
"description": "Choisissez une compétence. Si vous faites 6 ou moins à votre jet, vous considérez que votre jet est un 6. *Ne fonctionne pas sur les jets de fabrications et les jets d'œuvres*",
"description": "Choisissez une compétence. Si vous faites 6 ou moins à votre jet, vous considérez que votre jet est un 6. *Ne fonctionne pas sur les jets de fabrications et les jets d'œuvres* #todo",
"id": "7ii1ig85j7a1gacorzkn6oyjdt3w6jzh",
"effect": [
{
"id": "v0lf1gwsairuei43r3u3eyc5v57segtc",
"category": "choice",
"text": "Vous ne pouvez pas faire moins de 6 sur vos jets de ",
"options": [
{
"id": "cr0vgpzpca3gcjfiqfq4yy4ta14n6fjr",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "34ghl5vv2rwmr0zxznqee4wld2ng9q94",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "xceir7gw2atr8om3dt89jbjjihijbhre",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "0sg0evb589nlihsoleqvchdp5orqx9dv",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "7sdozpx5qmrisc7wcxule5w7fk931rxi",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "z482fmbjy7i5r1g51ie223392rb161gi",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "nd02a9m53ypyk3jen1mzt1nn9spj92dx",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "g3h2igg57rxpchnyg53jxk64vggzjcs4",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "hdeskp2s6zlw4xcfcaal5svo907bozhc",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "i9ie2gpcmn2pzxzz2dx4n0il92n68s6o",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "804h26knc7eh06s1t94x5gfg9i57sikz",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "luf7aprc50wr15wdrt1w1mimj3wgp2f6",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "a9ie2rutp48b0zjfvrjmz4rff3r7ujqi",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "rxrlyyf2azjs04fg4lno6oaao3fqwp2l",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "zw6ifv9b5a62asi43p8jtll8y34tfihq",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "ugxqcqnsl6u76tbv1hl0nvykeabp3a5f",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "dxpo0dq6qy48n1plte8ii2n7z4pg6tlw",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
}
]
"options": []
}
]
},
@ -7343,48 +7203,74 @@
"options": [
{
"text": "Modifieur de force",
"effects": [
{
"category": "value",
"property": "modifier/strength",
"operation": "add",
"value": 1
}
]
},
{
"text": "Modifieur de dextérité",
"effects": [
{
"category": "value",
"property": "modifier/dexterity",
"operation": "add",
"value": 1
}
]
},
{
"text": "Modifieur de constitution",
"effects": [
{
"category": "value",
"property": "modifier/constitution",
"operation": "add",
"value": 1
}
]
},
{
"text": "Modifieur d'intelligence",
"effects": [
{
"category": "value",
"property": "modifier/intelligence",
"operation": "add",
"value": 1
}
]
},
{
"text": "Modifieur de curiosité",
"effects": [
{
"category": "value",
"property": "modifier/curiosity",
"operation": "add",
"value": 1
}
]
},
{
"text": "Modifieur de charisme",
"effects": [
{
"category": "value",
"property": "modifier/charisma",
"operation": "add",
"value": 1
}
]
},
{
"text": "Modifieur de psyché",
"effects": [
{
"egory": "value",
"property": "modifier/psyche",
"operation": "add",
@ -7393,6 +7279,8 @@
]
}
]
}
]
},
"8v1duhxatene2utj8llwcj5eie4not77": {
"description": "Vous êtes capable de fabriquer des objets magiques d'une rareté accrue.",
@ -8225,47 +8113,14 @@
]
},
"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.",
"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",
"id": "s5kidncgfzw85ffubl718lx2f68suhqf",
"effect": [
{
"id": "yjc9xk64ygtc5tugluia031nhxgxvi6z",
"category": "choice",
"text": "Vous gagnez le premier niveau de la branche de ",
"options": [
{
"id": "l8b2fdpvjpihjeaqj1rs3el5w5jj0zww",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "xxscipqcvk2q5q97a3c926t523x6awth",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "i413jzkxi0tdjjj1aaz6m0bkm94uqahf",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "82pzzioqy1whd59xfdaiw88osvqqbqjw",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
}
]
"options": []
},
{
"id": "pzgqz28pnupmfmcf6mc7wmuhry775f7f",
@ -8394,47 +8249,14 @@
]
},
"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.",
"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",
"id": "qf3eru17f8u3hysq56k246mlq7p2rbc9",
"effect": [
{
"id": "460k5ti0iesdfc8j4mlh6nrdrzg67g6f",
"category": "choice",
"text": "Vous gagnez un niveau dans la branche de ",
"options": [
{
"id": "l8b2fdpvjpihjeaqj1rs3el5w5jj0zww",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "xxscipqcvk2q5q97a3c926t523x6awth",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "i413jzkxi0tdjjj1aaz6m0bkm94uqahf",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "82pzzioqy1whd59xfdaiw88osvqqbqjw",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
}
]
"options": []
},
{
"id": "8qddimnu5vwleys9fjoq84ju3d09ejpq",
@ -8563,47 +8385,14 @@
]
},
"sw45zzv7bf6v35h064f6zhcj1e7xbbr5": {
"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.",
"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",
"id": "sw45zzv7bf6v35h064f6zhcj1e7xbbr5",
"effect": [
{
"id": "pbpmdu5tgvi1saopqseq5mj7qqml3z3k",
"category": "choice",
"text": "Vous gagnez un niveau dans la branche de ",
"options": [
{
"id": "l8b2fdpvjpihjeaqj1rs3el5w5jj0zww",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "xxscipqcvk2q5q97a3c926t523x6awth",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "i413jzkxi0tdjjj1aaz6m0bkm94uqahf",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
},
{
"id": "82pzzioqy1whd59xfdaiw88osvqqbqjw",
"category": "value",
"text": "",
"operation": "add",
"property": "",
"value": 0
}
]
"options": []
},
{
"id": "qpq7g3m86jfpaopm1jofyfz6j69wk2nq",
@ -9262,54 +9051,82 @@
"options": [
{
"text": "Force",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/strength"
}
]
},
{
"text": "Dextérité",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/dexterity"
}
]
},
{
"text": "Constitution",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/constitution"
}
]
},
{
"text": "Intelligence",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/intelligence"
}
]
},
{
"text": "Curiosité",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/curiosity"
}
]
},
{
"text": "Charisme",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/charisma"
}
]
},
{
"text": "Psyché",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/psyche"
}
]
}
]
},
{
"category": "value",
@ -9910,53 +9727,81 @@
"options": [
{
"text": "Force",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/strength"
}
]
},
{
"text": "Dextérité",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/dexterity"
}
]
},
{
"text": "Constitution",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/constitution"
}
]
},
{
"text": "Intelligence",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/intelligence"
}
]
},
{
"text": "Curiosité",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/curiosity"
}
]
},
{
"text": "Charisme",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/charisma"
}
]
},
{
"text": "Psyché",
"effects": [
{
"category": "value",
"operation": "add",
"value": 1,
"property": "modifier/psyche"
}
]
}
],
"text": "+1 au modifieur de "
},
@ -9992,12 +9837,12 @@
},
"dx5khvrhwkhhn8fv4b8pecuh8i5wtwij": {
"id": "dx5khvrhwkhhn8fv4b8pecuh8i5wtwij",
"description": "",
"description": "le Temps des Tempetes",
"effect": []
},
"pfzopr4oyrsgxg0cbva16zzzly3kke9z": {
"id": "pfzopr4oyrsgxg0cbva16zzzly3kke9z",
"description": "",
"description": "pour Audible",
"effect": []
},
"fk0wmg94tlq78khq8zot2o5u4nnxr2gb": {

View File

@ -1,12 +1,14 @@
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel } from "~/types/character";
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel } from "~/types/character";
import { z } from "zod/v4";
import characterConfig from '#shared/character-config.json';
import { fakeA } from "#shared/proses";
import { button, input, loading, numberpicker, select, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, text } from "#shared/dom.util";
import { followermenu, tooltip } from "#shared/floating.util";
import proses, { preview } from "#shared/proses";
import { button, buttongroup, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, span, text } from "#shared/dom.util";
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
import { clamp } from "#shared/general.util";
import markdownUtil from "#shared/markdown.util";
import markdown from "#shared/markdown.util";
import { getText } from "./i18n";
import type { User } from "~/types/auth";
const config = characterConfig as CharacterConfig;
@ -27,7 +29,7 @@ export const defaultCharacter: Character = {
people: undefined,
level: 1,
training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0 }; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
training: MAIN_STATS.reduce((p, v) => { p[v] = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 }; return p; }, {} as Record<MainStat, Partial<Record<TrainingLevel, number>>>),
leveling: { 1: 0 },
abilities: {},
choices: {},
@ -38,6 +40,7 @@ export const defaultCharacter: Character = {
items: [],
exhaustion: 0,
sickness: [],
poisons: [],
},
owner: -1,
@ -111,7 +114,10 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
magicelement: 0,
magicinstinct: 0,
},
bonus: {},
bonus: {
abilities: {},
defense: {},
},
resistance: {},
initiative: 0,
capacity: 0,
@ -123,7 +129,7 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
spells: [],
},
aspect: "",
notes: character.notes ?? "",
notes: Object.assign({ public: '', private: '' }, character.notes),
});
export const mainStatTexts: Record<MainStat, string> = {
@ -156,6 +162,10 @@ export const elementTexts: Record<SpellElement, { class: string, text: string }>
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
};
export const elementDom = (element: SpellElement) => dom("span", {
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class],
text: elementTexts[element].text
});
export const alignmentTexts: Record<Alignment, string> = {
'loyal_good': 'Loyal bon',
@ -203,6 +213,22 @@ export const resistanceTexts: Record<Resistance, string> = {
'instinct': 'Sorts d\'instinct',
};
export const CharacterVariablesValidation = z.object({
health: z.number(),
mana: z.number(),
exhaustion: z.number(),
sickness: z.array(z.object({
id: z.string(),
state: z.number().min(1).max(7).or(z.literal(true)),
})),
poisons: z.array(z.object({
id: z.string(),
state: z.number().min(1).max(7).or(z.literal(true)),
})),
spells: z.array(z.string()),
items: z.array(z.string()),
});
export const CharacterValidation = z.object({
id: z.number(),
name: z.string(),
@ -214,18 +240,7 @@ export const CharacterValidation = z.object({
leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()),
abilities: z.record(z.enum(ABILITIES), z.number().optional()),
choices: z.record(z.string(), z.array(z.number())),
variables: z.object({
health: z.number(),
mana: z.number(),
exhaustion: z.number(),
sickness: z.array(z.object({
id: z.string(),
state: z.number().min(1).max(7).or(z.literal(true)),
})),
spells: z.array(z.string()),
equipment: z.array(z.string()),
}),
variables: CharacterVariablesValidation,
owner: z.number(),
username: z.string().optional(),
visibility: z.enum(["public", "private"]),
@ -239,6 +254,7 @@ export class CharacterCompiler
protected _character!: Character;
protected _result!: CompiledCharacter;
protected _buffer: Record<string, PropertySum> = {};
private _variableDirty: boolean = false;
constructor(character: Character)
{
@ -297,6 +313,20 @@ export class CharacterCompiler
{
this._character.variables[prop] = value;
this._result.variables[prop] = value;
this._variableDirty = true;
}
saveVariables()
{
if(this._variableDirty)
{
this._variableDirty = false;
useRequestFetch()(`/api/character/${this.character.id}/variables`, {
method: 'POST',
body: this._character.variables,
}).then(() => {}).catch(() => {
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
})
}
}
protected add(feature?: string)
{
@ -339,7 +369,7 @@ export class CharacterCompiler
const choice = this._character.choices[feature.id];
if(choice)
choice.forEach(e => this.apply(feature.options[e]!));
choice.forEach(e => feature.options[e]!.effects.forEach(this.apply.bind(this)));
return;
default:
@ -373,7 +403,7 @@ export class CharacterCompiler
const choice = this._character.choices[feature.id];
if(choice)
choice.forEach(e => this.undo(feature.options[e]!));
choice.forEach(e => feature.options[e]!.effects.forEach(this.undo.bind(this)));
return;
default:
@ -394,13 +424,16 @@ export class CharacterCompiler
if(buffer && buffer._dirty === true)
{
let sum = 0, shortcut = false;
for(let i = 0; i < buffer.list.length; i++)
for(let j = 0; j < buffer.list.length; j++)
{
if(typeof buffer.list[i]!.value === 'string') // Add or set a modifier
if(typeof buffer.list[j]!.value === 'string') // Add or set a modifier
{
const modifier = this._buffer[buffer.list[i]!.value as string];
const modifier = this._buffer[buffer.list[j]!.value as string];
if(!modifier)
{
if(!queue.includes(buffer.list[j]!.value as string))
this._buffer[buffer.list[j]!.value as string] = { _dirty: false, list: [], min: -Infinity, value: 0 };
queue.push(property);
shortcut = true;
break;
@ -408,29 +441,29 @@ export class CharacterCompiler
else if(modifier._dirty)
{
//Put it back in queue since its dependencies haven't been resolved yet
queue.push(buffer.list[i]!.value as string);
queue.push(buffer.list[j]!.value as string);
queue.push(property);
shortcut = true;
break;
}
else
{
if(buffer.list[i]?.operation === 'add')
if(buffer.list[j]?.operation === 'add')
sum += modifier.value;
else if(buffer.list[i]?.operation === 'set')
else if(buffer.list[j]?.operation === 'set')
sum = modifier.value;
else if(buffer.list[i]?.operation === 'min')
else if(buffer.list[j]?.operation === 'min')
this._buffer[property]!.min = modifier.value;
}
}
else
{
if(buffer.list[i]?.operation === 'add')
sum += buffer.list[i]!.value as number;
else if(buffer.list[i]?.operation === 'set')
sum = buffer.list[i]!.value as number;
else if(buffer.list[i]?.operation === 'min')
this._buffer[property]!.min = buffer.list[i]!.value as number;
if(buffer.list[j]?.operation === 'add')
sum += buffer.list[j]!.value as number;
else if(buffer.list[j]?.operation === 'set')
sum = buffer.list[j]!.value as number;
else if(buffer.list[j]?.operation === 'min')
this._buffer[property]!.min = buffer.list[j]!.value as number;
}
}
@ -726,6 +759,7 @@ class PeoplePicker extends BuilderTab
this._options = Object.values(config.peoples).map(
(people, i) => dom("div", { class: "flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35 cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]", listeners: { click: () => {
this._builder.character.people = people.id;
this._builder.character = { ...this._builder.character, people: people.id };
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.forEach(f => f?.classList.toggle(e, false)));
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options[i]?.classList.toggle(e, true));
}
@ -880,7 +914,7 @@ class TrainingPicker extends BuilderTab
return dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50 relative"], listeners: { click: e => {
this._builder.toggleTrainingOption(stat, parseInt(level[0]) as TrainingLevel, j);
this.update();
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }), choice ]);
}}}, [ markdown(config.features[option]!.description, undefined, { tags: { a: preview } }), choice ]);
}))
]);
}
@ -1136,3 +1170,411 @@ class AspectPicker extends BuilderTab
return true;
}
}
export class CharacterSheet
{
user: ComputedRef<User | null>;
character?: CharacterCompiler;
container: HTMLElement = div();
tabs?: HTMLDivElement & { refresh: () => void };
constructor(id: string, user: ComputedRef<User | null>)
{
this.user = user;
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
this.container.replaceChildren(load);
useRequestFetch()(`/api/character/${id}`).then(character => {
if(character)
{
this.character = new CharacterCompiler(character);
document.title = `d[any] - ${character.name}`;
load.remove();
this.render();
}
else
throw new Error();
}).catch(() => {
this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
span('text-2xl font-bold tracking-wider', 'Personnage introuvable'),
span(undefined, 'Ce personnage n\'existe pas ou est privé.'),
div('flex flex-row gap-4 justify-center items-center', [
button(text('Personnages publics'), () => useRouter().push({ name: 'character-list' }), 'px-2 py-1'),
button(text('Créer un personange'), () => useRouter().push({ name: 'character-id-edit', params: { id: 'new' } }), 'px-2 py-1')
])
]))
});
}
render()
{
if(!this.character)
return;
const character = this.character.compiled;
this.tabs = tabgroup([
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) },
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
] },
{ id: 'notes', title: [ text('Notes') ], content: () => [
] },
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } });
this.container.replaceChildren(div('flex flex-col justify-center gap-1', [
div("flex flex-row gap-4 justify-between", [
div(),
div("flex lg:flex-row flex-col gap-6 items-center justify-center", [
div("flex gap-6 items-center", [
div('inline-flex select-none items-center justify-center overflow-hidden align-middle h-16', [
div('text-light-100 dark:text-dark-100 leading-1 flex p-4 items-center justify-center bg-light-25 dark:bg-dark-25 font-medium', [
icon("radix-icons:person", { width: 16, height: 16 }),
])
]),
div("flex flex-col", [
dom("span", { class: "text-xl font-bold", text: character.name }),
dom("span", { class: "text-sm", text: `De ${character.username}` })
]),
div("flex flex-col", [
dom("span", { class: "font-bold", text: `Niveau ${character.level}` }),
dom("span", { text: config.peoples[character.race]?.name ?? 'Peuple inconnu' })
])
]),
div("flex flex-row lg:border-l border-light-35 dark:border-dark-35 py-4 ps-4 gap-8", [
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("PV: "),
dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: `${character.health - character.variables.health}`
}),
text(`/ ${character.health}`)
]),
dom("span", { class: "flex flex-row items-center gap-2 text-3xl font-light" }, [
text("Mana: "),
dom("span", {
class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35",
text: `${character.mana - character.variables.mana}`
}),
text(`/ ${character.mana}`)
])
])
]),
div("self-center", [
this.user.value && this.user.value.id === character.owner ?
button(icon("radix-icons:pencil-2"), () => useRouter().push({ name: 'character-id-edit', params: { id: this.character?.character.id } }), "p-1")
: div()
])
]),
div("flex flex-row justify-center 2xl:gap-4 gap-2 p-4 border-b border-light-35 dark:border-dark-35", [
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.strength}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Force" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.dexterity}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Dextérité" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.constitution}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Constitution" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.intelligence}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Intelligence" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.curiosity}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Curiosité" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.charisma}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Charisme" })
]),
div("flex flex-col items-center px-2", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.modifier.psyche}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Psyché" })
])
]),
div('border-l border-light-35 dark:border-dark-35'),
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
div("flex flex-col px-2 items-center", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: `+${character.initiative}` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Initiative" })
]),
div("flex flex-col px-2 items-center", [
dom("span", { class: "2xl:text-2xl text-xl font-bold", text: character.speed === false ? "Aucun déplacement" : `${character.speed} cases` }),
dom("span", { class: "text-sm 2xl:text-base", text: "Course" })
])
]),
div('border-l border-light-35 dark:border-dark-35'),
div("flex 2xl:gap-4 gap-2 flex-row items-center justify-between", [
icon("game-icons:checked-shield", { width: 32, height: 32 }),
div("flex flex-col px-2 items-center", [
dom("span", {
class: "2xl:text-2xl text-xl font-bold",
text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.passiveparry, 0, character.defense.hardcap)}`
}),
dom("span", { class: "text-sm 2xl:text-base", text: "Passive" })
]),
div("flex flex-col px-2 items-center", [
dom("span", {
class: "2xl:text-2xl text-xl font-bold",
text: `${clamp(character.defense.static + character.defense.passivedodge + character.defense.activeparry, 0, character.defense.hardcap)}`
}),
dom("span", { class: "text-sm 2xl:text-base", text: "Blocage" })
]),
div("flex flex-col px-2 items-center", [
dom("span", {
class: "2xl:text-2xl text-xl font-bold",
text: `${clamp(character.defense.static + character.defense.activedodge + character.defense.passiveparry, 0, character.defense.hardcap)}`
}),
dom("span", { class: "text-sm 2xl:text-base", text: "Esquive" })
])
]),
]),
div("flex flex-1 flex-row items-stretch justify-center py-2 gap-4", [
div("flex flex-col gap-4 py-1 w-60", [
div("flex flex-col py-1 gap-4", [
div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
]),
div("grid grid-cols-2 gap-2",
Object.entries(character.abilities).map(([ability, value]) =>
div("flex flex-row px-1 justify-between items-center", [
span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate", abilityTexts[ability as Ability] || ability),
span("font-bold text-base text-light-100 dark:text-dark-100", `+${value}`),
])
)
),
div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
]),
character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', size: 'small' }) : undefined,
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', size: 'small' }) : undefined,
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', size: 'small' }) : undefined,
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', size: 'small' }) : undefined,
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', size: 'small' }) : undefined,
character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', size: 'small' }) : undefined,
character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', size: 'small' }) : undefined,
character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', size: 'small' }) : undefined,
character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', size: 'small' }) : undefined,
character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', size: 'small' }) : undefined,
character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', size: 'small' }) : undefined,
character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', size: 'small' }) : undefined,
]) : undefined,
character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', size: 'small' }) : undefined,
character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', size: 'small' }) : undefined,
character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', size: 'small' }) : undefined,
]) : undefined,
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', character.spellranks.precision.toString()) ]),
div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', character.spellranks.knowledge.toString()) ]),
div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', character.spellranks.instinct.toString()) ]),
div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', character.spellranks.arts.toString()) ]),
])
])
]),
div('border-l border-light-35 dark:border-dark-35'),
this.tabs,
])
]));
}
actionsTab(character: CompiledCharacter)
{
return [
div('flex flex-col gap-8', [
div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
]),
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(e), undefined, { tags: { a: preview } }),
])) ?? [])
]),
]),
div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
]),
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
markdown(getText(e), undefined, { tags: { a: preview } }),
])) ?? [])
]),
]),
div('flex flex-col gap-2', [
div("flex flex-row items-center justify-center gap-4", [
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
]),
div('flex flex-col gap-2', [
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
markdown(getText(e), undefined, { tags: { a: preview } }),
])) ?? [])
]),
]),
]),
]
}
abilitiesTab(character: CompiledCharacter)
{
return [
div('flex flex-col gap-2', [
...(character.lists.passive?.map(e => div('flex flex-col gap-1', [
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
markdown(getText(e), undefined, { tags: { a: preview } }),
])) ?? []),
]),
];
}
spellTab(character: CompiledCharacter)
{
let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element';
const sort = (spells: Array<{ id: string, spell?: SpellConfig, source: string }>) => {
spells = spells.filter(e => !!e.spell);
switch(sortPreference)
{
case 'rank': return spells.sort((a, b) => a.spell!.rank - b.spell!.rank || SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!));
case 'type': return spells.sort((a, b) => a.spell!.type.localeCompare(b.spell!.type) || a.spell!.rank - b.spell!.rank);
case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!) || a.spell!.rank - b.spell!.rank);
default: return spells;
}
};
const spells = sort([...(character.lists.spells ?? []).map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'feature' })), ...character.variables.spells.map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'player' }))]).map(e => ({...e, dom:
e.spell ? div('flex flex-col gap-2', [
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${e.spell.cost ?? 0} mana` }) ]),
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]),
div('flex flex-row gap-2', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ])
]),
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.effect) ]),
]) : undefined }));
return [
div('flex flex-col gap-2', [
div('flex flex-row justify-between items-center', [
div('flex flex-row gap-2 items-center', [
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; this.tabs?.refresh(); } }),
]),
div('flex flex-row gap-2 items-center', [
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
button(text('Modifier'), () => this.spellPanel(character, spells), 'py-1 px-4'),
])
]),
div('flex flex-col gap-2', spells.map(e => e.dom))
])
]
}
spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>)
{
const availableSpells = Object.values(config.spells).filter(spell => {
if (spell.rank === 4) return false;
if (character.spellranks[spell.type] < spell.rank) return false;
return true;
});
const textAmount = text(character.variables.spells.length.toString()), textMax = text(character.spellslots.toString());
const container = div("border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-4 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]", [
div("flex flex-row justify-between items-center mb-4", [
dom("h2", { class: "text-xl font-bold", text: "Ajouter un sort" }),
div('flex flex-row gap-4 items-center', [ dom('span', { class: 'italic text-light-70 dark:text-dark-70 text-sm' }, [ textAmount, text(' / '), textMax, text(' sorts maitrisés') ]), tooltip(button(icon("radix-icons:cross-1", { width: 20, height: 20 }), () => {
setTimeout(blocker.close, 150);
container.setAttribute('data-state', 'inactive');
}, "p-1"), "Fermer", "left") ])
]),
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', availableSpells.map(spell => {
let state = character.lists.spells?.includes(spell.id) ? 'given' : character.variables.spells.includes(spell.id) ? 'choosen' : 'empty';
const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => {
if(state === 'choosen')
{
this.character!.variable('spells', character.variables.spells.filter(e => e !== spell.id));
state = 'empty';
}
else if(state === 'empty')
{
this.character!.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
state = 'choosen';
}
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
textAmount.textContent = character.variables.spells.length.toString();
this.tabs?.refresh();
}, "px-2 py-1 text-sm font-normal");
toggleButton.disabled = state === 'given';
return foldable(() => [
markdown(spell.effect),
], [ div("flex flex-row justify-between gap-2", [
dom("span", { class: "text-lg font-bold", text: spell.name }),
div("flex flex-row items-center gap-6", [
div("flex flex-row text-sm gap-2",
spell.elements.map(el =>
dom("span", {
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[el].class],
text: elementTexts[el].text
})
)
),
div("flex flex-row text-sm gap-1", [
...(spell.rank !== 4 ? [
dom("span", { text: `Rang ${spell.rank}` }),
text("/"),
dom("span", { text: spellTypeTexts[spell.type] }),
text("/")
] : []),
dom("span", { text: `${spell.cost} mana` }),
text("/"),
dom("span", { text: typeof spell.speed === "string" ? spell.speed : `${spell.speed} minutes` })
]),
toggleButton,
]),
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
}))
]);
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
}
}

View File

@ -35,9 +35,16 @@ export function async(size: 'small' | 'normal' | 'large' = 'normal', fn: Promise
}
export function button(content: Node, onClick?: () => void, cls?: Class)
{
const btn = 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
/*
text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: () => disabled || (onClick && onClick()) } }, [ content ]);
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50
*/
const btn = dom('button', { class: [`inline-flex justify-center items-center outline-none leading-none transition-[box-shadow]
text-light-100 dark:text-dark-100 bg-light-20 dark:bg-dark-20 border border-light-40 dark:border-dark-40
hover:bg-light-25 dark:hover:bg-dark-25 hover:border-light-50 dark:hover:border-dark-50
focus:bg-light-30 dark:focus:bg-dark-30 focus:border-light-50 dark:focus:border-dark-50 focus:shadow-raw focus:shadow-light-50 dark:focus:shadow-dark-50
disabled:text-light-50 dark:disabled:text-dark-50 disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-dashed disabled:border-light-40 dark:disabled:border-dark-40`, cls], listeners: { click: () => disabled || (onClick && onClick()) } }, [ content ]);
let disabled = false;
Object.defineProperty(btn, 'disabled', {
get: () => disabled,
@ -48,6 +55,26 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
})
return btn;
}
export function buttongroup<T extends any>(options: Array<{ text: string, value: T }>, settings?: { class?: { container?: Class, option?: Class }, value?: T, onChange?: (value: T) => boolean | void })
{
let currentValue = settings?.value;
const elements = options.map(e => dom('div', { class: [`cursor-pointer text-light-100 dark:text-dark-100 hover:bg-light-30 dark:hover:bg-dark-30 flex items-center justify-center bg-light-20 dark:bg-dark-20 leading-none outline-none
border border-light-40 dark:border-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[selected]:z-10 data-[selected]:border-light-50 dark:data-[selected]:border-dark-50
data-[selected]:shadow-raw transition-[box-shadow] data-[selected]:shadow-light-50 dark:data-[selected]:shadow-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40`,
settings?.class?.option], text: e.text, attributes: { 'data-selected': settings?.value === e.value }, listeners: { click: function() {
if(currentValue !== e.value)
{
elements.forEach(e => e.toggleAttribute('data-selected', false));
this.toggleAttribute('data-selected', true);
if(!settings?.onChange || settings?.onChange(e.value) !== false)
{
currentValue = e.value;
}
}
}}}))
return div(['flex flex-row', settings?.class?.container], elements);
}
export type Option<T> = { text: string, render?: () => HTMLElement, value: T | Option<T>[] } | undefined;
type StoredOption<T> = { item: Option<T>, dom: HTMLElement, container?: HTMLElement, children?: Array<StoredOption<T>> };
export function select<T extends NonNullable<any>>(options: Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
@ -434,6 +461,39 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
return element;
}
export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: NodeChildren | (() => NodeChildren) }>, settings?: { focused?: string, class?: { container?: Class, tabbar?: Class, title?: Class, content?: Class } }): HTMLDivElement & { refresh: () => void }
{
let focus = settings?.focused ?? tabs[0]?.id;
const titles = tabs.map((e, i) => dom('div', { class: ['px-2 py-1 border-b border-transparent hover:border-accent-blue data-[focus]:border-accent-blue data-[focus]:border-b-[3px] cursor-pointer', settings?.class?.title], attributes: { 'data-focus': e.id === focus }, listeners: { click: function() {
if(this.hasAttribute('data-focus'))
return;
titles.forEach(e => e.toggleAttribute('data-focus', false));
this.toggleAttribute('data-focus', true);
focus = e.id;
const _content = typeof e.content === 'function' ? e.content() : e.content;
//@ts-expect-error
content.replaceChildren(..._content);
}}}, e.title));
const _content = tabs.find(e => e.id === focus)?.content;
const content = div(['', settings?.class?.content], typeof _content === 'function' ? _content() : _content);
const container = div(['flex flex-col', settings?.class?.container], [
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
content
]);
Object.defineProperty(container, 'refresh', {
writable: false,
configurable: false,
enumerable: false,
value: () => {
const _content = tabs.find(e => e.id === focus)?.content;
//@ts-expect-error
_content && content.replaceChildren(...(typeof _content === 'function' ? _content() : _content))
}
})
return container as HTMLDivElement & { refresh: () => void };
}
export interface ToastConfig
{

View File

@ -4,9 +4,9 @@ export type Node = HTMLElement | SVGElement | Text | undefined;
export type NodeChildren = Array<Node>;
export type Class = string | Array<Class> | Record<string, boolean> | undefined;
type Listener<K extends keyof HTMLElementEventMap> = | ((ev: HTMLElementEventMap[K]) => any) | {
type Listener<K extends keyof HTMLElementEventMap> = | ((this: HTMLElement, ev: HTMLElementEventMap[K]) => any) | {
options?: boolean | AddEventListenerOptions;
listener: (ev: HTMLElementEventMap[K]) => any;
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
} | undefined;
export interface NodeProperties
@ -42,9 +42,9 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
{
const key = k as keyof HTMLElementEventMap, value = v as Listener<typeof key>;
if(typeof value === 'function')
element.addEventListener(key, value);
element.addEventListener(key, value.bind(element));
else if(value)
element.addEventListener(key, value.listener, value.options);
element.addEventListener(key, value.listener.bind(element), value.options);
}
}
@ -56,6 +56,10 @@ export function div(cls?: Class, children?: NodeChildren): HTMLDivElement
{
return dom("div", { class: cls }, children);
}
export function span(cls?: Class, text?: string): HTMLSpanElement
{
return dom("span", { class: cls, text: text });
}
export function svg<K extends keyof SVGElementTagNameMap>(tag: K, properties?: NodeProperties, children?: Omit<NodeChildren, 'HTMLElement' | 'Text'>): SVGElementTagNameMap[K]
{
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
@ -134,7 +138,7 @@ export function icon(name: string, properties?: IconProperties): HTMLElement
properties?.mode && el.setAttribute('mode', properties?.mode.toString());
properties?.inline && el.toggleAttribute('inline', properties?.inline);
properties?.noobserver && el.toggleAttribute('noobserver', properties?.noobserver);
el.toggleAttribute('noobserver', properties?.noobserver ?? true);
properties?.width && el.setAttribute('width', properties?.width.toString());
properties?.height && el.setAttribute('height', properties?.height.toString());
properties?.flip && el.setAttribute('flip', properties?.flip.toString());

View File

@ -1,7 +1,7 @@
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureEffect, FeatureItem, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
import type { Ability, AspectConfig, CharacterConfig, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel } from "~/types/character";
import { div, dom, icon, text, type NodeChildren } from "#shared/dom.util";
import { MarkdownEditor } from "#shared/editor.util";
import { fakeA } from "#shared/proses";
import { preview } from "#shared/proses";
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
@ -65,7 +65,7 @@ export class HomebrewBuilder
const promise: Promise<Feature> = this._editor.edit(feature).then(f => {
this._config.features[feature.id] = f;
return f;
}).catch(() => feature).finally(() => {
}).catch((e) => { if(e) console.error(e); return feature; }).finally(() => {
setTimeout(popup.close, 150);
this._editor.container.setAttribute('data-state', 'inactive');
});
@ -133,7 +133,7 @@ class PeopleEditor extends BuilderTab
const render = (people: string, level: Level, feature: string) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[feature]!).then(e => {
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }));
});
}, contextmenu: (e) => {
e.preventDefault();
@ -154,7 +154,7 @@ class PeopleEditor extends BuilderTab
}
}) } } }, [ text('Supprimer') ]) : undefined,
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]);
return element;
}
const peopleRender = (people: RaceConfig) => {
@ -180,7 +180,7 @@ class TrainingEditor extends BuilderTab
const render = (stat: MainStat, level: TrainingLevel, feature: string) => {
let element = dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
this._builder.edit(config.features[feature]!).then(e => {
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }));
element.replaceChildren(markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }));
});
}, contextmenu: (e) => {
e.preventDefault();
@ -201,7 +201,7 @@ class TrainingEditor extends BuilderTab
}
}) } } }, [ text('Supprimer') ]) : undefined,
], { placement: "right-start", priority: false });
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: fakeA } }) ]);
}}}, [ markdownUtil(config.features[feature]!.description, undefined, { tags: { a: preview } }) ]);
return element;
};
const statRenderBlock = (stat: MainStat) => {
@ -411,7 +411,7 @@ export class FeatureEditor
private _renderEffect(effect: Partial<FeatureItem>): HTMLDivElement
{
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: preview } }) ]),
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
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'), () => {
@ -458,14 +458,14 @@ export class FeatureEditor
switch(buffer.category)
{
case 'value':
const valueVariable = () => typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } });
const valueVariable = () => typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as FeatureValue).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as FeatureValue).value = value; summaryText.textContent = textFromEffect(buffer); } });
const summaryText = text(textFromEffect(buffer));
let valueSelection = valueVariable();
top = [
select([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].includes(buffer.property ?? '') || ['defense/'].some(e => (buffer as Extract<FeatureEffect, { category: "value" }>).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }),
select([ (['action', 'reaction'].includes(buffer.property ?? '') ? undefined : { text: '+', value: 'add' }), (['speed', 'capacity', 'action', 'reaction'].includes(buffer.property ?? '') || ['defense/'].some(e => (buffer as FeatureValue).property.startsWith(e))) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as FeatureValue).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-[80px]' } }),
valueSelection,
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 FeatureValue).value = (typeof (buffer as FeatureValue).value === 'number' ? '' as any as false : 0);
const newValueSelection = valueVariable();
valueSelection.replaceWith(newValueSelection);
valueSelection = newValueSelection;
@ -479,13 +479,13 @@ export class FeatureEditor
{
if(buffer.list === 'spells')
{
bottom = [ combobox(config.spells.map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ];
bottom = [ combobox(config.spells.map(e => ({ text: e.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: e.name, class: 'font-bold' }), div('flex flex-row gap-8', [ dom('span', { class: 'italic', text: `Rang ${e.rank === 4 ? 'spécial' : e.rank}` }), dom('span', { text: spellTypeTexts[e.type] }) ]) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderText(e.effect)) ]) ]), value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as FeatureList).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 hover:z-10 h-[36px] w-full hover:outline-px outline-light-50 dark:outline-dark-50 !border-none' }, fill: 'contain' }) ];
}
else
{
const editor = new MarkdownEditor();
editor.content = getText(buffer.item);
editor.onChange = (item) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = item;
editor.onChange = (item) => (buffer as FeatureList).item = item;
bottom = [ div('px-2 py-1 bg-light-25 dark:bg-dark-25 flex-1 flex items-center', [ editor.dom ]) ];
}
@ -495,7 +495,7 @@ export class FeatureEditor
bottom = [ combobox(Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add').map(e => ({ text: buffer.list !== 'spells' ? renderText(getText((e as Extract<FeatureItem, { category: 'list' }>).item)) : config.spells.find(f => f.id === (e as Extract<FeatureItem, { category: 'list' }>).item)?.name ?? '', value: (e as Extract<FeatureItem, { category: 'list' }>).item })), { defaultValue: buffer.item, change: (item) => buffer.item = item, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full overflow-hidden truncate', option: 'max-h-[90px] text-sm' }, fill: 'contain' }) ];
}
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 FeatureList).action = value as 'add' | 'remove';
const element = redraw();
content.replaceWith(element);
content = element;
@ -503,14 +503,15 @@ export class FeatureEditor
break;
case 'choice':
const add = () => {
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);
const option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; } = { effects: [{ id: getID() }], text: '' };
(buffer as FeatureChoice).options.push(option as FeatureChoice["options"][number]);
list.appendChild(render(option, true));
};
const render = (option: FeatureEffect & { text: string }, state: boolean): HTMLElement => {
const { top: _top, bottom: _bottom } = drawByCategory(option);
const render = (option: { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; }, state: boolean): HTMLElement => {
/* const { top: _top, bottom: _bottom } = drawByCategory(option);
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 { text: string; effects: (Partial<FeatureValue | FeatureList>)[]; };
const element = render(option, true);
_content.replaceWith(element);
_content = element;
@ -519,7 +520,7 @@ export class FeatureEditor
_content.remove();
(buffer as Extract<FeatureItem, { category: 'choice' }>).options = (buffer as Extract<FeatureItem, { category: 'choice' }>).options.filter(e => e.id !== option.id);
}, 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Supprimer', 'bottom') ]) ], { class: { title: 'border-b border-light-35 dark:border-dark-35', icon: 'w-[34px] h-[34px]', content: 'border-b border-light-35 dark:border-dark-35' }, open: state });
return _content;
return _content; */
}
const list = div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35 gap-2', buffer.options?.map(e => render(e, false)) ?? []);
top = [ input('text', { defaultValue: buffer.text, input: (value) => (buffer as Extract<FeatureItem, { category: 'choice' }>).text = value, class: 'bg-light-25 dark:bg-dark-25 !-m-px hover:z-10 h-[36px] w-full', placeholder: 'Description' }), tooltip(button(icon('radix-icons:plus'), () => add(), 'px-2 -m-px hover:z-10 border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), 'Ajouter une option', 'bottom') ];
@ -590,43 +591,43 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
] },
{ text: 'Compétences', value: [
...ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
{ text: 'Max de compétence', value: ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
{ text: 'Max de compétence', value: ABILITIES.map((e) => ({ text: `Max > ${abilityTexts[e as Ability]}`, value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
] },
{ text: 'Modifieur', value: [
{ text: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } },
{ text: 'Modifieur de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } },
{ text: 'Modifieur de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } },
{ text: 'Modifieur d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', 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 psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } },
{ text: 'Modifieur au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [
{ text: 'Modifieur de force', category: 'value', property: 'modifier/strength', operation: 'add', value: 1 },
{ text: 'Modifieur de dextérité', category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 },
{ text: 'Modifieur de constitution', category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 },
{ text: 'Modifieur d\'intelligence', category: 'value', property: 'modifier/intelligence', 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 psyché', egory: 'value', property: 'modifier/psyche', operation: 'add', value: 1 }
{ text: 'Mod. de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } },
{ text: 'Mod. de dextérité', value: { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } },
{ text: 'Mod. de constitution', value: { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } },
{ text: 'Mod. d\'intelligence', value: { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } },
{ text: 'Mod. de curiosité', value: { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } },
{ text: 'Mod. de charisme', value: { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } },
{ text: 'Mod. de psyché', value: { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } },
{ text: 'Mod. au choix', value: { category: 'choice', text: '+1 au modifieur de ', options: [
{ text: 'Mod. de force', effects: [ { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } ] },
{ text: 'Mod. de dextérité', effects: [ { category: 'value', property: 'modifier/dexterity', operation: 'add', value: 1 } ] },
{ text: 'Mod. de constitution', effects: [ { category: 'value', property: 'modifier/constitution', operation: 'add', value: 1 } ] },
{ text: 'Mod. d\'intelligence', effects: [ { category: 'value', property: 'modifier/intelligence', operation: 'add', value: 1 } ] },
{ text: 'Mod. de curiosité', effects: [ { category: 'value', property: 'modifier/curiosity', operation: 'add', value: 1 } ] },
{ text: 'Mod. de charisme', effects: [ { category: 'value', property: 'modifier/charisma', operation: 'add', value: 1 } ] },
{ text: 'Mod. de psyché', effects: [ { category: 'value', property: 'modifier/psyche', operation: 'add', value: 1 } ] }
]} as Partial<FeatureItem>}
] },
{ text: 'Jet de résistance', value: [
{ text: 'Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } },
{ text: 'Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } },
{ text: 'Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 } },
{ text: 'Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', 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: 'Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } },
{ text: 'Résistance > Force', value: { category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 } },
{ text: 'Résistance > Dextérité', value: { category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 } },
{ text: 'Résistance > Constitution', value: { category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 } },
{ text: 'Résistance > Intelligence', value: { category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 } },
{ text: 'Résistance > Curiosité', value: { category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 } },
{ text: 'Résistance > Charisme', value: { category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 } },
{ text: 'Résistance > Psyché', value: { category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 } },
{ text: 'Résistance au choix', value: { category: 'choice', text: '+1 au jet de résistance de ', options: [
{ text: 'Force', category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 },
{ text: 'Dextérité', category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 },
{ text: 'Constitution', category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 },
{ text: 'Intelligence', category: 'value', property: 'bonus/defense/intelligence', 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: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }
]} as Partial<FeatureItem>}
{ text: 'Résistance > Force', effects: [{ category: 'value', property: 'bonus/defense/strength', operation: 'add', value: 1 }] },
{ text: 'Résistance > Dextérité', effects: [{ category: 'value', property: 'bonus/defense/dexterity', operation: 'add', value: 1 }] },
{ text: 'Résistance > Constitution', effects: [{ category: 'value', property: 'bonus/defense/constitution', operation: 'add', value: 1 }] },
{ text: 'Résistance > Intelligence', effects: [{ category: 'value', property: 'bonus/defense/intelligence', operation: 'add', value: 1 }] },
{ text: 'Résistance > Curiosité', effects: [{ category: 'value', property: 'bonus/defense/curiosity', operation: 'add', value: 1 }] },
{ text: 'Résistance > Charisme', effects: [{ category: 'value', property: 'bonus/defense/charisma', operation: 'add', value: 1 }] },
{ text: 'Résistance > Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }] }
]} as Partial<FeatureChoice>}
] },
{ text: 'Bonus', value: RESISTANCES.map(e => ({ text: resistanceTexts[e as Resistance], value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) },
{ text: 'Rang', value: [
@ -643,7 +644,7 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
{ text: 'Choix', value: { category: 'choice', text: '', options: [] }, },
];
const flattenFeatureChoices = Tree.accumulate(featureChoices, 'value', (item) => Array.isArray(item.value) ? undefined : item.value).filter(e => !!e) as Partial<FeatureItem>[];
function textFromEffect(effect: Partial<FeatureItem>): string
function textFromEffect(effect: Partial<FeatureItem | FeatureEquipment>): string
{
if(effect.category === 'value')
{

View File

@ -30,6 +30,7 @@ export interface ModalProperties
{
priority?: boolean;
closeWhenOutside?: boolean;
onClose?: () => boolean | void;
}
let teleport: HTMLDivElement;
@ -161,6 +162,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
function link(element: HTMLElement) {
Object.entries({
'mouseenter': show,
'mousemove': show,
'mouseleave': hide,
'focus': show,
'blur': hide,
@ -296,14 +298,13 @@ export function tooltip(container: HTMLElement, txt: string | Text, placement: F
export function fullblocker(content: NodeChildren, properties?: ModalProperties)
{
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } });
const close = () => (!properties?.onClose || properties.onClose() !== false) && _modal.remove();
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (close) : undefined } });
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);
teleport.appendChild(_modal);
return {
close: () => _modal.remove(),
}
return { close };
}
export function modal(content: NodeChildren, properties?: ModalProperties)
{

View File

@ -1,4 +1,4 @@
import { dom, icon, type NodeChildren, type Node, div } from "#shared/dom.util";
import { dom, icon, type NodeChildren, type Node, div, type Class } from "#shared/dom.util";
import { parseURL } from 'ufo';
import render from "#shared/markdown.util";
import { popper } from "#shared/floating.util";
@ -20,7 +20,7 @@ export const a: Prose = {
const link = overview ? { name: 'explore-path', params: { path: overview.path }, hash: hash } : href, nav = router.resolve(link);
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', properties?.class], attributes: { href: nav.href }, listeners: {
'click': (e) => {
e.preventDefault();
router.push(link);
@ -45,7 +45,7 @@ export const a: Prose = {
return [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown')
{
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' });
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
}
if(_content?.type === 'canvas')
{
@ -63,37 +63,34 @@ export const a: Prose = {
return el;
}
}
export const fakeA: Prose = {
custom(properties, children) {
export const preview: Prose = {
custom(properties: { href: string, class?: Class, size?: 'small' | 'large' }, children) {
const href = properties.href as string;
const { hash, pathname } = parseURL(href);
const router = useRouter();
const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname);
const el = dom('span', { class: 'cursor-pointer text-accent-blue inline-flex items-center' }, [
dom('span', {}, [
const el = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [
...(children ?? []),
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
])
]);
if(!!overview)
{
const magicKeys = useMagicKeys();
popper(el, {
return overview ? popper(el, {
arrow: true,
delay: 150,
offset: 12,
cover: "height",
placement: 'bottom-start',
class: 'data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full z-[45]',
class: ['data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 w-full z-[45]',
{ 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]': !properties?.size || properties.size === 'large', 'max-w-[400px] max-h-[250px]': properties.size === 'small' }
],
content: () => {
return [async('large', Content.getContent(overview.id).then((_content) => {
if(_content?.type === 'markdown')
{
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto py-4 px-6' });
return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' });
}
if(_content?.type === 'canvas')
{
@ -108,10 +105,7 @@ export const fakeA: Prose = {
if(!magicKeys.current.has('control') || magicKeys.current.has('meta'))
return false;
},
});
}
return el;
}) : el;
}
}
export const callout: Prose = {

73
types/character.d.ts vendored
View File

@ -1,4 +1,5 @@
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES } from "#shared/character.util";
import type { Localized } from "#shared/general";
export type MainStat = typeof MAIN_STATS[number];
export type Ability = typeof ABILITIES[number];
@ -13,14 +14,22 @@ export type Resistance = typeof RESISTANCES[number];
export type FeatureID = string;
export type i18nID = string;
export type RecursiveKeyOf<TObj extends object> = {
[TKey in keyof TObj & (string | number)]:
TObj[TKey] extends any[] ? `${TKey}` :
TObj[TKey] extends object
? `${TKey}` | `${TKey}/${RecursiveKeyOf<TObj[TKey]>}`
: `${TKey}`;
}[keyof TObj & (string | number)];
export type Character = {
id: number;
name: string;
people?: string;
name: string; //Free text
people?: string; //People ID
level: number;
aspect?: number;
notes?: string | null;
notes?: { public?: string, private?: string }; //Free text
training: Record<MainStat, Partial<Record<TrainingLevel, number>>>;
leveling: Partial<Record<Level, number>>;
@ -38,11 +47,12 @@ export type CharacterVariables = {
exhaustion: number;
sickness: Array<{ id: string, state: number | true }>;
poisons: Array<{ id: string, state: number | true }>;
spells: string[]; //Spell ID
items: ItemState[];
};
type ItemState = {
id: string,
id: string;
amount: number;
enchantments?: [];
charges?: number;
@ -55,11 +65,16 @@ export type CharacterConfig = {
spells: SpellConfig[];
aspects: AspectConfig[];
features: Record<FeatureID, Feature>;
enchantments: Record<string, { name: string, effect: FeatureEffect[], power: number }>; //TODO
enchantments: Record<string, EnchantementConfig>; //TODO
items: Record<string, ItemConfig>;
lists: Record<string, { id: string, name: string, [key: string]: any }[]>;
lists: Record<string, { id: string, config: Record<string, any>, values: Record<string, any> }>;
texts: Record<i18nID, Localized>;
};
export type EnchantementConfig = {
name: string; //TODO -> TextID
effect: Array<FeatureEquipment | FeatureValue | FeatureList>;
power: number;
}
export type ItemConfig = CommonItemConfig & (ArmorConfig | WeaponConfig | WondrousConfig | MundaneConfig);
type CommonItemConfig = {
id: string;
@ -87,7 +102,7 @@ type WondrousConfig = {
category: 'wondrous';
name: string; //TODO -> TextID
description: i18nID;
effect: FeatureEffect[];
effect: FeatureItem[];
};
type MundaneConfig = {
category: 'mundane';
@ -96,25 +111,25 @@ type MundaneConfig = {
};
export type SpellConfig = {
id: string;
name: string;
name: string; //TODO -> TextID
rank: 1 | 2 | 3 | 4;
type: SpellType;
cost: number;
speed: "action" | "reaction" | number;
elements: Array<SpellElement>;
effect: string;
effect: string; //TODO -> TextID
concentration: boolean;
tags?: string[];
};
export type RaceConfig = {
id: string;
name: string;
description: string;
name: string; //TODO -> TextID
description: string; //TODO -> TextID
options: Record<Level, FeatureID[]>;
};
export type AspectConfig = {
name: string;
description: string;
description: string; //TODO -> TextID
stat: MainStat | 'special';
alignment: Alignment;
magic: boolean;
@ -122,33 +137,42 @@ export type AspectConfig = {
physic: { min: number, max: number };
mental: { min: number, max: number };
personality: { min: number, max: number };
options: FeatureEffect[];
options: FeatureItem[];
};
export type FeatureEffect = {
export type FeatureValue = {
id: FeatureID;
category: "value";
operation: "add" | "set" | "min";
property: string;
property: RecursiveKeyOf<CompiledCharacter> | 'spec' | 'ability' | 'training';
value: number | `modifier/${MainStat}` | false;
} | {
}
export type FeatureEquipment = {
id: FeatureID;
category: "value";
operation: "add" | "set" | "min";
property: 'weapon/damage' | 'armor/health' | 'armor/absorb/flat' | 'armor/absorb/percent';
value: number | `modifier/${MainStat}` | false;
}
export type FeatureList = {
id: FeatureID;
category: "list";
list: "spells" | "sickness" | "action" | "reaction" | "freeaction" | "passive";
action: "add" | "remove";
item: string;
item: string | i18nID;
extra?: any;
};
export type FeatureItem = FeatureEffect | {
export type FeatureChoice = {
id: FeatureID;
category: "choice";
text: string;
text: string; //TODO -> TextID
settings?: { //If undefined, amount is 1 by default
amount: number;
exclusive: boolean; //Disallow to pick the same option twice
};
options: Array<FeatureEffect & { text: string }>;
options: Array<{ text: string, effects: Array<FeatureValue | FeatureList> }>; //TODO -> TextID
};
export type FeatureItem = FeatureValue | FeatureList | FeatureChoice;
export type Feature = {
id: FeatureID;
description: i18nID;
@ -199,13 +223,16 @@ export type CompiledCharacter = {
magicinstinct: number;
};
bonus: Record<string, number>; //Any special bonus goes here
bonus: {
defense: Partial<Record<MainStat, number>>;
abilities: Partial<Record<Ability, number>>;
}; //Any special bonus goes here
resistance: Record<string, number>;
modifier: Record<MainStat, number>;
abilities: Partial<Record<Ability, number>>;
level: number;
lists: { [K in Extract<FeatureEffect, { category: "list" }>["list"]]?: string[] };
lists: { [K in FeatureList['list']]?: string[] }; //string => ListItem ID
notes: string;
notes: { public: string, private: string };
};