Compare commits

...

2 Commits

13 changed files with 898 additions and 673 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>

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

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')
{
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 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)
{
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

@ -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])
])
]);

File diff suppressed because it is too large Load Diff

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 { 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 proses, { preview } from "#shared/proses";
import { button, buttongroup, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
import { div, dom, icon, text } from "#shared/dom.util";
import { followermenu, tooltip } from "#shared/floating.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;
@ -339,7 +341,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 +375,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:
@ -726,6 +728,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 +883,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 +1139,375 @@ class AspectPicker extends BuilderTab
return true;
}
}
export class CharacterSheet
{
character?: CharacterCompiler;
container: HTMLElement = div();
constructor(id: string, user: ComputedRef<User | null>)
{
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
{
//ERROR
}
});
}
render()
{
if(!this.character)
return;
const character = this.character.compiled;
console.log(character);
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", [
/* user && user.id === character.owner ?
button(icon("radix-icons:pencil-2"), () => {
}, "icon")
: 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-80", [
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', class: 'h-4' }) ]),
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
]),
div("grid grid-cols-3 gap-2",
Object.entries(character.abilities).map(([ability, value]) =>
div("flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70", [
dom("span", { class: "font-bold text-base text-light-100 dark:text-dark-100", text: `+${value}` }),
dom("span", { text: abilityTexts[ability as Ability] || ability })
])
)
),
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', 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' }) : undefined,
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet' }) : undefined,
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles' }) : undefined,
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes' }) : undefined,
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées' }) : undefined,
character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes' }) : undefined,
character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains' }) : undefined,
character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables' }) : undefined,
character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles' }) : undefined,
character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues' }) : undefined,
character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers' }) : undefined,
character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains' }) : 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' }) : undefined,
character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures' }) : undefined,
character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes' }) : 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'), dom('span', { text: character.spellranks.precision.toString(), class: 'font-bold' }) ]),
div('flex flex-row items-center gap-2', [ text('Savoir'), dom('span', { text: character.spellranks.knowledge.toString(), class: 'font-bold' }) ]),
div('flex flex-row items-center gap-2', [ text('Instinct'), dom('span', { text: character.spellranks.instinct.toString(), class: 'font-bold' }) ]),
div('flex flex-row items-center gap-2', [ text('Oeuvres'), dom('span', { text: character.spellranks.arts.toString(), class: 'font-bold' }) ]),
])
])
]),
div('border-l border-light-35 dark:border-dark-35'),
tabgroup([
{ id: 'actions', title: [ text('Actions') ], content: () => [
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 } }),
])) ?? [])
]),
]),
]),
] },
{ 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]' } }),
])
]));
}
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)
{
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([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: 'rank', class: { option: 'px-2 py-1 text-sm' } }),
])
])
])
]
}
spellPanel()
{
if(!this.character)
return;
const character = this.character.compiled;
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)); //TO REWORK
state = 'empty';
}
else if(state === 'empty')
{
//this.character.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
state = 'choosen';
}
//character = compiler.compiled; //TO REWORK
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
textAmount.textContent = character.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);
}
}

View File

@ -48,6 +48,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 })
{
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))
{
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 +454,26 @@ 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 } })
{
const 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);
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);
return div(['flex flex-col', settings?.class?.container], [
div(['flex flex-row items-center gap-1', settings?.class?.tabbar], titles),
content
]);
}
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, children?: NodeChildren): HTMLSpanElement
{
return dom("span", { class: cls }, children);
}
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);

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

@ -20,8 +20,8 @@ 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: {
'click': (e) => {
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,7 +63,7 @@ export const a: Prose = {
return el;
}
}
export const fakeA: Prose = {
export const preview: Prose = {
custom(properties, children) {
const href = properties.href as string;
const { hash, pathname } = parseURL(href);
@ -71,11 +71,9 @@ export const fakeA: Prose = {
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', {}, [
...(children ?? []),
overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined
])
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
]);
@ -93,7 +91,7 @@ export const fakeA: 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')
{

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: ListConfig, 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 };
};