Compare commits
2 Commits
423df7bc42
...
1642cd513f
| Author | SHA1 | Date |
|---|---|---|
|
|
1642cd513f | |
|
|
b1ac379f1a |
|
|
@ -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-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -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>
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
Loading…
Reference in New Issue