Add back Loading Indicator, rework children caching, small visual improvement on character sheet and config management.
This commit is contained in:
parent
0eaffcaa04
commit
f761e44569
13
app/app.vue
13
app/app.vue
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<div class="text-light-100 dark:text-dark-100 flex bg-light-0 dark:bg-dark-0 h-screen overflow-hidden">
|
||||
<NuxtRouteAnnouncer/>
|
||||
<TooltipProvider>
|
||||
<NuxtLayout>
|
||||
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</TooltipProvider>
|
||||
<NuxtLoadingIndicator :throttle="50"/>
|
||||
<NuxtLayout>
|
||||
<div class="xl:px-12 xl:py-8 lg:px-8 lg:py-6 px-6 py-3 flex flex-1 justify-center overflow-auto max-h-full max-w-full relative" id="mainContainer">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function create()
|
|||
useRequestFetch()('/api/campaign', {
|
||||
method: 'POST',
|
||||
body: { name: 'Margooning', public_notes: '', dm_notes: '', settings: {} },
|
||||
}).then(() => Toaster.add({ duration: 8000, content: 'Campagne créée', type: 'info' })).catch((e) => Toaster.add({ duration: 8000, title: 'Une erreur est survenue', content: e, type: 'error' }))
|
||||
}).then((result) => Toaster.add({ duration: 8000, content: 'Campagne créée', type: 'info' })).catch((e) => Toaster.add({ duration: 8000, title: 'Une erreur est survenue', content: e, type: 'error' }))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -87,20 +87,6 @@ function create()
|
|||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="campaign of campaigns.filter(e => e.status === 'ARCHIVED')">
|
||||
<NuxtLink :to="{ name: 'campaign-id', params: { id: campaign.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||
<div class="flex flex-1 flex-col gap-2 justify-center">
|
||||
<span class="text-lg font-bold group-hover:text-accent-blue">{{ campaign.name }}</span>
|
||||
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
||||
<div class="flex flex-row flex-1 items-stretch gap-4">
|
||||
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="archives && archives.length > 0" class="flex flex-row w-full gap-8 justify-center items-center"><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span><span class="text-lg font-semibold">Archives</span><span class="border-t border-light-35 dark:border-dark-35 flex-1"></span></div>
|
||||
<div v-if="archives && archives.length > 0" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
||||
|
|
@ -136,20 +122,6 @@ function create()
|
|||
</AlertDialogRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="campaign of campaigns.filter(e => e.status === 'ARCHIVED')">
|
||||
<NuxtLink :to="{ name: 'campaign-id', params: { id: campaign.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||
<div class="flex flex-1 flex-col gap-2 justify-center">
|
||||
<span class="text-lg font-bold group-hover:text-accent-blue">{{ campaign.name }}</span>
|
||||
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
||||
<div class="flex flex-row flex-1 items-stretch gap-4">
|
||||
<span class="text-sm">{{ campaign.members.length }} joueur{{ campaign.members.length === 1 ? '' : 's' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-2 items-center flex-1">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES, DAMAGE_TYPES, WEAPON_TYPES } from "#shared/character.util";
|
||||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES, DAMAGE_TYPES, WEAPON_TYPES, PropertySum, ITEM_BUFFER_KEYS } from "#shared/character.util";
|
||||
import type { Localized } from "../types/general";
|
||||
|
||||
export type MainStat = typeof MAIN_STATS[number];
|
||||
|
|
@ -57,11 +57,16 @@ export type CharacterVariables = {
|
|||
|
||||
money: number;
|
||||
};
|
||||
export type TreeStructure = {
|
||||
|
||||
name: string;
|
||||
};
|
||||
type CommonState = {
|
||||
capacity?: number;
|
||||
powercost?: number;
|
||||
};
|
||||
type ArmorState = { health?: number };
|
||||
type StateBufferKeys = typeof ITEM_BUFFER_KEYS[number];
|
||||
type ArmorState = { loss: number, health?: number, absorb?: { flat?: number, percent?: number } };
|
||||
type WeaponState = { attack?: number | string, hit?: number };
|
||||
type WondrousState = { };
|
||||
type MundaneState = { };
|
||||
|
|
@ -72,11 +77,12 @@ type ItemState = {
|
|||
charges?: number;
|
||||
equipped?: boolean;
|
||||
state?: (ArmorState | WeaponState | WondrousState | MundaneState) & CommonState;
|
||||
buffer?: Partial<Record<StateBufferKeys, PropertySum>>;
|
||||
};
|
||||
export type CharacterConfig = {
|
||||
peoples: Record<string, RaceConfig>;
|
||||
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
|
||||
spells: Record<string, SpellConfig | ArtConfig>;
|
||||
spells: Record<string, SpellConfig>;
|
||||
aspects: Record<string, AspectConfig>;
|
||||
features: Record<FeatureID, Feature>;
|
||||
enchantments: Record<string, EnchantementConfig>;
|
||||
|
|
@ -86,11 +92,12 @@ export type CharacterConfig = {
|
|||
freeaction: Record<string, { id: string, name: string, description: string }>;
|
||||
passive: Record<string, { id: string, name: string, description: string }>;
|
||||
texts: Record<i18nID, Localized>;
|
||||
trees: Record<string, TreeStructure>;
|
||||
|
||||
//Each of these groups extend an existing feature as they all use the same properties
|
||||
sickness: Record<FeatureID, { stage: number }>; //TODO
|
||||
poisons: Record<FeatureID, { difficulty: number, efficienty: number, solubility: number }>; //TODO
|
||||
dedications: Record<FeatureID, { id: string, name: string, description: i18nID, effect: FeatureID[], requirement: Array<{ stat: MainStat, amount: number }> }>; //TODO
|
||||
sickness: Record<FeatureID, { name: string, stage: number }>; //TODO
|
||||
poisons: Record<FeatureID, { name: string, difficulty: number, efficienty: number, solubility: number }>; //TODO
|
||||
dedications: Record<FeatureID, { name: string, requirement: Array<{ stat: MainStat, amount: number }> }>; //TODO
|
||||
};
|
||||
export type EnchantementConfig = {
|
||||
id: string;
|
||||
|
|
@ -142,7 +149,7 @@ export type SpellConfig = {
|
|||
id: string;
|
||||
name: string; //TODO -> TextID
|
||||
rank: 1 | 2 | 3 | 4;
|
||||
type: Exclude<SpellType, "arts">;
|
||||
type: SpellType;
|
||||
cost: number;
|
||||
speed: "action" | "reaction" | number;
|
||||
elements: Array<SpellElement>;
|
||||
|
|
@ -191,7 +198,7 @@ export type FeatureEquipment = {
|
|||
id: FeatureID;
|
||||
category: "value";
|
||||
operation: "add" | "set" | "min";
|
||||
property: 'weapon/damage/value' | 'armor/health' | 'armor/absorb/flat' | 'armor/absorb/percent';
|
||||
property: StateBufferKeys;
|
||||
value: number | `modifier/${MainStat}` | false;
|
||||
}
|
||||
export type FeatureList = {
|
||||
|
|
@ -228,13 +235,15 @@ export type CompiledCharacter = {
|
|||
race: string;
|
||||
spellslots: number; //Max
|
||||
artslots: number; //Max
|
||||
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;
|
||||
spellranks: Record<SpellType | 'arts', 0 | 1 | 2 | 3>;
|
||||
aspect: {
|
||||
id: string,
|
||||
amount: number;
|
||||
duration: number;
|
||||
bonus: number;
|
||||
shift_bonus: number;
|
||||
tier: 0 | 1 | 2;
|
||||
|
||||
bonus?: Partial<CompiledCharacter['bonus']>;
|
||||
};
|
||||
speed: number | false;
|
||||
capacity: number | false;
|
||||
|
|
@ -269,13 +278,13 @@ export type CompiledCharacter = {
|
|||
defense: Partial<Record<MainStat, number>>; //Defense aux jets de resistance
|
||||
abilities: Partial<Record<Ability, number>>;
|
||||
spells: {
|
||||
type: Partial<Record<SpellType, number>>;
|
||||
type: Partial<Record<SpellType | 'arts', number>>;
|
||||
rank: Partial<Record<1 | 2 | 3 | 4, number>>;
|
||||
elements: Partial<Record<SpellElement, number>>;
|
||||
};
|
||||
weapon: Partial<Record<WeaponType, number>>;
|
||||
resistance: Partial<Record<Resistance, number>>; //Bonus à l'attaque
|
||||
}; //Any special bonus goes here
|
||||
resistance: Partial<Record<Resistance, number>>; //Bonus à l'attaque
|
||||
|
||||
craft: { level: number, bonus: number };
|
||||
|
||||
|
|
|
|||
|
|
@ -199,5 +199,8 @@ export default defineNuxtConfig({
|
|||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag === 'iconify-icon',
|
||||
}
|
||||
},
|
||||
devtools: {
|
||||
enabled: false,
|
||||
}
|
||||
})
|
||||
|
|
@ -10,7 +10,7 @@ export default defineEventHandler(async (e) => {
|
|||
if(!body.success)
|
||||
{
|
||||
setResponseStatus(e, 400);
|
||||
return body.error.message;
|
||||
throw body.error.message;
|
||||
}
|
||||
|
||||
const session = await getUserSession(e);
|
||||
|
|
@ -39,7 +39,7 @@ export default defineEventHandler(async (e) => {
|
|||
});
|
||||
|
||||
setResponseStatus(e, 201);
|
||||
return id;
|
||||
return { id, link: cryptURI('campaign', id) };
|
||||
}
|
||||
catch(_e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export class CampaignSheet
|
|||
div('flex flex-row gap-2 items-center py-2', [
|
||||
div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), campaign.owner.username, 'bottom') ]),
|
||||
div('border-l h-full w-0 border-light-40 dark:border-dark-40'),
|
||||
div('flex flex-row gap-1', { list: campaign.members, render: (member) => div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), member.member.username, 'bottom') ]) }),
|
||||
div('flex flex-row gap-1', { list: campaign.members, render: (member, _c) => _c ?? div('w-8 h-8 relative flex items-center justify-center border border-light-40 dark:border-dark-40 box-content rounded-full', [ tooltip(icon('radix-icons:person', { width: 24, height: 24, class: 'text-light-70 dark:text-dark-70' }), member.member.username, 'bottom') ]) }),
|
||||
]),
|
||||
div('flex flex-1 flex-col items-center justify-center gap-2', [
|
||||
span('text-2xl font-serif font-bold italic', campaign.name),
|
||||
|
|
@ -178,7 +178,7 @@ export class CampaignSheet
|
|||
div('flex flex-row gap-4 flex-1 h-0', [
|
||||
div('flex flex-col gap-2', [
|
||||
div('flex flex-row items-center gap-4 w-[320px]', [ span('font-bold text-lg', 'Etat'), div('border-t border-light-40 dark:border-dark-40 border-dashed flex-1') ]),
|
||||
div('flex flex-col gap-2', { list: this.characters, render: (e) => e.container }),
|
||||
div('flex flex-col gap-2', { list: this.characters, render: (e, _c) => _c ?? e.container }),
|
||||
div('px-8 py-4 w-full flex', [
|
||||
button([
|
||||
icon('radix-icons:plus-circled', { width: 24, height: 24 }),
|
||||
|
|
@ -218,7 +218,7 @@ export class CampaignSheet
|
|||
const _modal = modal([
|
||||
div('flex flex-col gap-4 items-center min-w-[480px] min-h-24', [
|
||||
span('text-xl font-bold', 'Mes personnages'),
|
||||
div('grid grid-cols-3 gap-2', { list: () => current.characters, render: (e) => div('border border-light-40 dark:border-dark-40 p-2 flex flex-col w-[140px]', [
|
||||
div('grid grid-cols-3 gap-2', { list: () => current.characters, render: (e, _c) => _c ?? div('border border-light-40 dark:border-dark-40 p-2 flex flex-col w-[140px]', [
|
||||
span('font-bold', e.name),
|
||||
span('', `Niveau ${e.level}`),
|
||||
button(text('Ajouter'), () => useRequestFetch()(`/api/character/${e.id}/campaign/${this.campaign!.id}`, { method: 'POST' }).then(() => this.ws!.send('character', { id: e.id, name: e.name, action: 'ADD', })).catch(e => Toaster.add({ duration: 15000, content: e.message ?? e, title: 'Une erreur est survenue', type: 'error' })).finally(_modal.close)),
|
||||
|
|
@ -257,7 +257,9 @@ export class CampaignSheet
|
|||
div('flex flex-row gap-1 items-center', [ span('italic text-sm', 'Argent'), this.user.value && this.user.value.id === this.campaign.owner.id ? money.readonly : div('cursor-pointer px-2 py-px flex flex-row gap-1 items-center', [ span('text-lg font-bold', () => this.campaign!.money.toLocaleString(undefined, { useGrouping: true })), icon('ph:coin', { width: 16, height: 16 }) ]) ]),
|
||||
])
|
||||
]),
|
||||
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: this.campaign.items, render: e => {
|
||||
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: this.campaign.items, render: (e, _c) => {
|
||||
if(_c) return _c;
|
||||
|
||||
const item = config.items[e.id];
|
||||
|
||||
if(!item) return;
|
||||
|
|
|
|||
|
|
@ -200,15 +200,13 @@ export class Node extends EventTarget
|
|||
{
|
||||
properties: CanvasNode;
|
||||
|
||||
nodeDom!: RedrawableHTML;
|
||||
nodeDom?: RedrawableHTML;
|
||||
|
||||
constructor(properties: CanvasNode)
|
||||
{
|
||||
super();
|
||||
|
||||
this.properties = properties;
|
||||
|
||||
this.getDOM()
|
||||
}
|
||||
|
||||
protected getDOM()
|
||||
|
|
@ -230,6 +228,13 @@ export class Node extends EventTarget
|
|||
}
|
||||
}
|
||||
|
||||
get dom()
|
||||
{
|
||||
if(this.nodeDom === undefined)
|
||||
this.getDOM();
|
||||
|
||||
return this.nodeDom;
|
||||
}
|
||||
get style()
|
||||
{
|
||||
return this.properties.color ? this.properties.color?.class ?
|
||||
|
|
@ -270,7 +275,7 @@ export class NodeEditable extends Node
|
|||
if(!this.dirty)
|
||||
return;
|
||||
|
||||
Object.assign(this.nodeDom.style, {
|
||||
Object.assign(this.nodeDom!.style, {
|
||||
transform: `translate(${this.properties.x}px, ${this.properties.y}px)`,
|
||||
width: `${this.properties.width}px`,
|
||||
height: `${this.properties.height}px`,
|
||||
|
|
@ -329,7 +334,7 @@ export class Edge extends EventTarget
|
|||
{
|
||||
properties: CanvasEdge;
|
||||
|
||||
edgeDom!: RedrawableHTML;
|
||||
edgeDom?: RedrawableHTML;
|
||||
protected from: Node;
|
||||
protected to: Node;
|
||||
protected path: Path;
|
||||
|
|
@ -344,8 +349,6 @@ export class Edge extends EventTarget
|
|||
this.to = to;
|
||||
this.path = getPath(this.from.properties, properties.fromSide, this.to.properties, properties.toSide)!;
|
||||
this.labelPos = labelCenter(this.from.properties, properties.fromSide, this.to.properties, properties.toSide);
|
||||
|
||||
this.getDOM();
|
||||
}
|
||||
|
||||
protected getDOM()
|
||||
|
|
@ -364,6 +367,13 @@ export class Edge extends EventTarget
|
|||
]);
|
||||
}
|
||||
|
||||
get dom()
|
||||
{
|
||||
if(this.edgeDom === undefined)
|
||||
this.getDOM();
|
||||
|
||||
return this.edgeDom;
|
||||
}
|
||||
get style()
|
||||
{
|
||||
return this.properties.color ? this.properties.color?.class ?
|
||||
|
|
@ -377,8 +387,8 @@ export class EdgeEditable extends Edge
|
|||
private focusing: boolean = false;
|
||||
private editing: boolean = false;
|
||||
|
||||
private pathDom!: SVGPathElement;
|
||||
private inputDom!: RedrawableHTML;
|
||||
private pathDom?: SVGPathElement;
|
||||
private inputDom?: RedrawableHTML;
|
||||
constructor(properties: CanvasEdge, from: NodeEditable, to: NodeEditable)
|
||||
{
|
||||
super(properties, from, to);
|
||||
|
|
@ -406,7 +416,7 @@ export class EdgeEditable extends Edge
|
|||
update()
|
||||
{
|
||||
this.path = getPath(this.from.properties, this.properties.fromSide, this.to.properties, this.properties.toSide)!;
|
||||
this.pathDom.setAttribute('d', this.path.path);
|
||||
this.pathDom!.setAttribute('d', this.path.path);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +479,7 @@ export class Canvas
|
|||
//const { loggedIn, user } = useUserSession();
|
||||
this.transform = dom('div', { class: 'origin-center h-full' }, [
|
||||
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
|
||||
dom('div', {}, this.nodes.map(e => e.nodeDom)), dom('div', {}, this.edges.map(e => e.edgeDom)),
|
||||
dom('div', {}, this.nodes.map(e => e.dom)), dom('div', {}, this.edges.map(e => e.dom)),
|
||||
])
|
||||
]);
|
||||
|
||||
|
|
@ -763,7 +773,7 @@ export class CanvasEditor extends Canvas
|
|||
|
||||
this.transform = dom('div', { class: 'origin-center h-full' }, [
|
||||
dom('div', { class: 'absolute top-0 left-0 w-full h-full pointer-events-none *:pointer-events-auto *:select-none touch-none' }, [
|
||||
dom('div', {}, [...this.nodes.map(e => e.nodeDom), this.nodeHelper]), dom('div', {}, [...this.edges.map(e => e.edgeDom)]),
|
||||
dom('div', {}, [...this.nodes.map(e => e.dom), this.nodeHelper]), dom('div', {}, [...this.edges.map(e => e.dom)]),
|
||||
]), this.edgeHelper,
|
||||
]);
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,19 +1,17 @@
|
|||
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, EnchantementConfig, FeatureItem, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
|
||||
import type { Ability, Alignment, ArmorConfig, ArmorState, Character, CharacterConfig, CharacterVariables, CompiledCharacter, DamageType, EnchantementConfig, FeatureEquipment, FeatureItem, ItemConfig, ItemState, Level, MainStat, MundaneState, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel, WeaponConfig, WeaponState, WeaponType, WondrousState } from "~/types/character";
|
||||
import { z } from "zod/v4";
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import proses, { a, preview } from "#shared/proses";
|
||||
import { async, button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
||||
import { button, buttongroup, checkbox, floater, foldable, input, loading, multiselect, numberpicker, optionmenu, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
||||
import { div, dom, icon, span, text, type RedrawableHTML } from "#shared/dom.util";
|
||||
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||
import { clamp } from "#shared/general.util";
|
||||
import markdown, { defaultProses, filterMarkdown, renderMarkdown } from "#shared/markdown.util";
|
||||
import markdown from "#shared/markdown.util";
|
||||
import { getText } from "#shared/i18n";
|
||||
import type { User } from "~/types/auth";
|
||||
import { MarkdownEditor } from "#shared/editor.util";
|
||||
import { Socket } from "#shared/websocket.util";
|
||||
import { raw, reactive } from '#shared/reactive';
|
||||
import { Content, type ContentMap, type LocalContent } from "./content.util";
|
||||
import type { Root } from "hast";
|
||||
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
|
|
@ -21,13 +19,14 @@ export const MAIN_STATS = ["strength","dexterity","constitution","intelligence",
|
|||
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const;
|
||||
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const;
|
||||
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12] as const;
|
||||
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const;
|
||||
export const SPELL_TYPES = ["precision","knowledge","instinct"] as const;
|
||||
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
|
||||
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
|
||||
export const ALIGNMENTS = ['loyal_good', 'neutral_good', 'chaotic_good', 'loyal_neutral', 'neutral_neutral', 'chaotic_neutral', 'loyal_evil', 'neutral_evil', 'chaotic_evil'] as const;
|
||||
export const RESISTANCES = ['stun','bleed','poison','fear','influence','charm','possesion','precision','knowledge','instinct'] as const;
|
||||
export const DAMAGE_TYPES = ['slashing', 'piercing', 'bludgening', 'magic', 'fire', 'thunder', 'cold'] as const;
|
||||
export const WEAPON_TYPES = ["light", "shield", "heavy", "classic", "throw", "natural", "twohanded", "finesse", "reach", "projectile"] as const;
|
||||
export const ITEM_BUFFER_KEYS = ['attack', 'hit', 'health', 'absorb/flat', 'absorb/percent'] as const;
|
||||
|
||||
export const defaultCharacter: Character = {
|
||||
id: -1,
|
||||
|
|
@ -128,9 +127,9 @@ const defaultCompiledCharacter = (character: Character) => ({
|
|||
type: {},
|
||||
rank: {},
|
||||
},
|
||||
weapon: {}
|
||||
weapon: {},
|
||||
resistance: {},
|
||||
},
|
||||
resistance: {},
|
||||
initiative: 0,
|
||||
capacity: 0,
|
||||
lists: {
|
||||
|
|
@ -144,7 +143,7 @@ const defaultCompiledCharacter = (character: Character) => ({
|
|||
id: character.aspect ?? "",
|
||||
duration: 0,
|
||||
amount: 0,
|
||||
bonus: 0,
|
||||
shift_bonus: 0,
|
||||
tier: 0,
|
||||
},
|
||||
advantages: [],
|
||||
|
|
@ -199,10 +198,10 @@ export const alignmentTexts: Record<Alignment, string> = {
|
|||
'neutral_evil': 'Neutre mauvais',
|
||||
'chaotic_evil': 'Chaotique mauvais',
|
||||
};
|
||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision" };
|
||||
export const abilityTexts: Record<Ability, string> = {
|
||||
"athletics": "Athlétisme",
|
||||
"acrobatics": "Acrobatique",
|
||||
"acrobatics": "Acrobatisme",
|
||||
"intimidation": "Intimidation",
|
||||
"sleightofhand": "Doigté",
|
||||
"stealth": "Discrétion",
|
||||
|
|
@ -290,9 +289,11 @@ export const CharacterValidation = z.object({
|
|||
});
|
||||
|
||||
type Property = { value: number | string | false, id: string, operation: "set" | "add" | "min" };
|
||||
type PropertySum = { list: Array<Property>, min: number, value: number, _dirty: boolean };
|
||||
export type PropertySum = { list: Array<Property>, min: number, value: number, _dirty: boolean };
|
||||
export class CharacterCompiler
|
||||
{
|
||||
private _dirty: boolean = true;
|
||||
|
||||
protected _character!: Character;
|
||||
protected _result!: CompiledCharacter;
|
||||
protected _buffer: Record<string, PropertySum> = {
|
||||
|
|
@ -324,6 +325,7 @@ export class CharacterCompiler
|
|||
'modifier/charisma': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||
'modifier/psyche': { value: 0, _dirty: false, min: -Infinity, list: [] },
|
||||
};
|
||||
this._dirty = true;
|
||||
|
||||
if(value.people !== undefined)
|
||||
{
|
||||
|
|
@ -334,6 +336,8 @@ export class CharacterCompiler
|
|||
});
|
||||
|
||||
Object.entries(value.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
|
||||
|
||||
value.variables.items.forEach((e) => this.update(e));
|
||||
}
|
||||
}
|
||||
get character(): Character
|
||||
|
|
@ -343,7 +347,8 @@ export class CharacterCompiler
|
|||
get compiled(): CompiledCharacter
|
||||
{
|
||||
Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
|
||||
this.compile(Object.keys(this._buffer));
|
||||
this._dirty && this.compile(Object.keys(this._buffer));
|
||||
this._dirty = false;
|
||||
|
||||
return this._result;
|
||||
}
|
||||
|
|
@ -352,7 +357,8 @@ export class CharacterCompiler
|
|||
Object.entries(this._character.abilities).forEach(e => this._result.abilities[e[0] as Ability] = e[1]);
|
||||
|
||||
const keys = Object.keys(this._buffer);
|
||||
this.compile(keys);
|
||||
this._dirty && this.compile(keys);
|
||||
this._dirty = false;
|
||||
|
||||
return keys.reduce((p, v) => {
|
||||
p[v] = this._buffer[v]!.value;
|
||||
|
|
@ -362,7 +368,7 @@ export class CharacterCompiler
|
|||
get armor()
|
||||
{
|
||||
const armors = this._character.variables.items.filter(e => e.equipped && config.items[e.id]?.category === 'armor');
|
||||
return armors.length > 0 ? armors.map(e => ({ max: (config.items[e.id] as ArmorConfig).health, current: (config.items[e.id] as ArmorConfig).health - ((e.state as ArmorState)?.health ?? 0) })).reduce((p, v) => { p.max += v.max; p.current += v.current; return p; }, { max: 0, current: 0 }) : undefined;
|
||||
return armors.length > 0 ? armors.map(e => ({ max: (config.items[e.id] as ArmorConfig).health, current: (config.items[e.id] as ArmorConfig).health - ((e.state as ArmorState)?.loss ?? 0) })).reduce((p, v) => { p.max += v.max; p.current += v.current; return p; }, { max: 0, current: 0 }) : undefined;
|
||||
}
|
||||
get weight()
|
||||
{
|
||||
|
|
@ -373,20 +379,28 @@ export class CharacterCompiler
|
|||
return this._character.variables.items.filter(e => config.items[e.id]?.equippable && e.equipped).reduce((p, v) => p + ((config.items[v.id]?.powercost ?? 0) + (v.enchantments?.reduce((_p, _v) => (config.enchantments[_v]?.power ?? 0) + _p, 0) ?? 0) * v.amount), 0);
|
||||
}
|
||||
|
||||
parse(text: string): string
|
||||
update(item: ItemState)
|
||||
{
|
||||
return text.replace(/\{(.*?)\}/gmi, (substring: string, group: string) => {
|
||||
console.log(substring, group);
|
||||
return substring;
|
||||
})
|
||||
item.buffer ??= {};
|
||||
|
||||
if(item.equipped)
|
||||
[...config.items[item.id]?.effects ?? [], ...item.enchantments?.flatMap(e => config.enchantments[e]?.effect) ?? []]?.forEach(e => this.apply(e, item));
|
||||
else
|
||||
[...config.items[item.id]?.effects ?? [], ...item.enchantments?.flatMap(e => config.enchantments[e]?.effect) ?? []]?.forEach(e => this.undo(e, item));
|
||||
|
||||
this.compile(Object.keys(item.buffer), item.buffer, item.state);
|
||||
this.compile(Object.keys(this._buffer));
|
||||
this.saveVariables();
|
||||
}
|
||||
saveVariables()
|
||||
{
|
||||
const variables = raw(this._character.variables);
|
||||
variables.items.forEach(e => delete e.buffer);
|
||||
clearTimeout(this._variableDebounce);
|
||||
this._variableDebounce = setTimeout(() => {
|
||||
useRequestFetch()(`/api/character/${this.character.id}/variables`, {
|
||||
method: 'POST',
|
||||
body: raw(this._character.variables),
|
||||
body: variables,
|
||||
}).then(() => {}).catch(() => {
|
||||
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
|
||||
})
|
||||
|
|
@ -406,20 +420,21 @@ export class CharacterCompiler
|
|||
if(!feature)
|
||||
return;
|
||||
|
||||
config.features[feature]?.effect.forEach(this.apply.bind(this));
|
||||
config.features[feature]?.effect.forEach((effect) => this.apply(effect));
|
||||
}
|
||||
protected remove(feature?: string)
|
||||
{
|
||||
if(!feature)
|
||||
return;
|
||||
|
||||
config.features[feature]?.effect.forEach(this.undo.bind(this));
|
||||
config.features[feature]?.effect.forEach((effect) => this.undo(effect));
|
||||
}
|
||||
protected apply(feature?: FeatureItem)
|
||||
protected apply(feature?: FeatureItem | FeatureEquipment, item?: ItemState)
|
||||
{
|
||||
if(!feature)
|
||||
return;
|
||||
|
||||
this._dirty = true;
|
||||
switch(feature.category)
|
||||
{
|
||||
case "list":
|
||||
|
|
@ -430,33 +445,35 @@ export class CharacterCompiler
|
|||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
|
||||
const target = (item && ITEM_BUFFER_KEYS.includes(feature.property as any) ? item.buffer : this._buffer) as Record<string, PropertySum>;
|
||||
target[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
|
||||
|
||||
this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
|
||||
target[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
|
||||
|
||||
this._buffer[feature.property]!.min = -Infinity;
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
target[feature.property]!.min = -Infinity;
|
||||
target[feature.property]!._dirty = true;
|
||||
|
||||
if(feature.property.startsWith('modifier/'))
|
||||
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
|
||||
Object.values(target).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
|
||||
|
||||
return;
|
||||
case "choice":
|
||||
const choice = this._character.choices[feature.id];
|
||||
|
||||
if(choice)
|
||||
choice.forEach(e => feature.options[e]!.effects.forEach(this.apply.bind(this)));
|
||||
choice.forEach(e => feature.options[e]!.effects.forEach((effect) => this.apply(effect)));
|
||||
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
protected undo(feature?: FeatureItem)
|
||||
protected undo(feature?: FeatureItem | FeatureEquipment, item?: ItemState)
|
||||
{
|
||||
if(!feature)
|
||||
return;
|
||||
|
||||
this._dirty = true;
|
||||
switch(feature.category)
|
||||
{
|
||||
case "list":
|
||||
|
|
@ -467,40 +484,42 @@ export class CharacterCompiler
|
|||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
|
||||
const target = (item && ITEM_BUFFER_KEYS.includes(feature.property as any) ? item.buffer : this._buffer) as Record<string, PropertySum>;
|
||||
target[feature.property] ??= { list: [], value: 0, _dirty: true, min: -Infinity };
|
||||
|
||||
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id), 1);
|
||||
const idx = target[feature.property]!.list.findIndex(e => e.id === feature.id);
|
||||
idx !== -1 && target[feature.property]!.list.splice(idx, 1);
|
||||
|
||||
this._buffer[feature.property]!.min = -Infinity;
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
target[feature.property]!.min = -Infinity;
|
||||
target[feature.property]!._dirty = true;
|
||||
|
||||
if(feature.property.startsWith('modifier/'))
|
||||
Object.values(this._buffer).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
|
||||
Object.values(target).forEach(e => e._dirty = e.list.some(f => f.value === feature.property) ? true : e._dirty);
|
||||
|
||||
return;
|
||||
case "choice":
|
||||
const choice = this._character.choices[feature.id];
|
||||
|
||||
if(choice)
|
||||
choice.forEach(e => feature.options[e]!.effects.forEach(this.undo.bind(this)));
|
||||
choice.forEach(e => feature.options[e]!.effects.forEach((effect) => this.undo(effect)));
|
||||
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
protected compile(queue: string[])
|
||||
protected compile(queue: string[], _buffer: Record<string, PropertySum> = this._buffer, _target: any = this._result)
|
||||
{
|
||||
for(let i = 0; i < queue.length; i++)
|
||||
{
|
||||
if(queue[i] === undefined || queue[i] === "") continue;
|
||||
|
||||
const property = queue[i]!;
|
||||
const buffer = this._buffer[property];
|
||||
const buffer = _buffer[property];
|
||||
|
||||
if(buffer && buffer._dirty === true)
|
||||
{
|
||||
let sum = 0, shortcut = false;
|
||||
let sum = 0, stop = false;
|
||||
for(let j = 0; j < buffer.list.length; j++)
|
||||
{
|
||||
const item = buffer.list[j];
|
||||
|
|
@ -509,13 +528,14 @@ export class CharacterCompiler
|
|||
|
||||
if(typeof item.value === 'string') // Add or set a modifier
|
||||
{
|
||||
const modifier = this._buffer[item.value as string]!;
|
||||
const modifier = _buffer[item.value as string]!;
|
||||
if(modifier._dirty)
|
||||
{
|
||||
//Put it back in queue since its dependencies haven't been resolved yet
|
||||
//Also put the dependency itself in the queue to make sure it actually get resolves someday
|
||||
queue.push(item.value as string);
|
||||
queue.push(property);
|
||||
shortcut = true;
|
||||
stop = true;
|
||||
break;
|
||||
}
|
||||
else
|
||||
|
|
@ -539,17 +559,17 @@ export class CharacterCompiler
|
|||
}
|
||||
}
|
||||
|
||||
if(shortcut === true)
|
||||
if(stop === true)
|
||||
continue;
|
||||
|
||||
const path = property.split("/");
|
||||
const object = path.length === 1 ? this._result : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, this._result as any);
|
||||
const object = path.length === 1 ? _target : path.slice(0, -1).reduce((p, v) => { p[v] ??= {}; return p[v]; }, _target as any);
|
||||
|
||||
if(object.hasOwnProperty(path.slice(-1)[0]!))
|
||||
object[path.slice(-1)[0]!] = Math.max(sum, this._buffer[property]!.min);
|
||||
object[path.slice(-1)[0]!] = Math.max(sum, _buffer[property]!.min);
|
||||
|
||||
this._buffer[property]!.value = Math.max(sum, this._buffer[property]!.min);
|
||||
this._buffer[property]!._dirty = false;
|
||||
_buffer[property]!.value = Math.max(sum, _buffer[property]!.min);
|
||||
_buffer[property]!._dirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -625,7 +645,7 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
this._content = dom('div', { class: 'flex-1 outline-none max-w-full w-full overflow-y-auto', attributes: { id: 'characterEditorContainer' } });
|
||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||
div("flex w-full flex-row gap-4 items-center justify-between px-4 bg-light-0 dark:bg-dark-0 z-20", [
|
||||
div('flex flex-row gap-2', [ floater(tooltip(button(icon('radix-icons:pencil-2', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes publics', 'left'), [ publicNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes publics', position: 'bottom-start' }), floater(tooltip(button(icon('radix-icons:eye-none', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes privés', 'right'), [ privateNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, title: 'Notes privés', position: 'bottom-start' }) ]), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div('flex flex-row gap-2', [ tooltip(button(icon("radix-icons:chevron-right", { height: 16, width: 16 }), () => this.next(), 'p-1'), 'Suivant', "bottom"), tooltip(button(icon("radix-icons:paper-plane", { height: 16, width: 16 }), () => this.save(), 'p-1'), 'Enregistrer', "bottom"), tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]),
|
||||
div('flex flex-row gap-2', [ floater(tooltip(button(icon('radix-icons:pencil-2', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes publics', 'left'), [ publicNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', title: 'Notes publics', position: 'bottom-start' }), floater(tooltip(button(icon('radix-icons:eye-none', { width: 16, height: 16 }), undefined, 'p-1'), 'Notes privés', 'right'), [ privateNotes.dom ], { pinned: true, events: { show: ['click'], hide: [] }, class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', title: 'Notes privés', position: 'bottom-start' }) ]), div("flex w-full flex-row gap-4 items-center justify-center relative", this._stepsHeader), div('flex flex-row gap-2', [ tooltip(button(icon("radix-icons:chevron-right", { height: 16, width: 16 }), () => this.next(), 'p-1'), 'Suivant', "bottom"), tooltip(button(icon("radix-icons:paper-plane", { height: 16, width: 16 }), () => this.save(), 'p-1'), 'Enregistrer', "bottom"), tooltip(icon("radix-icons:question-mark-circled", { height: 20, width: 20 }), this._helperText, "bottom-end") ]),
|
||||
]),
|
||||
this._content,
|
||||
]));
|
||||
|
|
@ -1317,7 +1337,7 @@ export const stateFactory = (item: ItemConfig) => {
|
|||
switch(item.category)
|
||||
{
|
||||
case 'armor':
|
||||
state.state = { health: 0 } as ArmorState;
|
||||
state.state = { loss: 0, health: 0, absorb: { flat: 0, percent: 0 } } as ArmorState;
|
||||
break;
|
||||
case 'mundane':
|
||||
state.state = { } as MundaneState;
|
||||
|
|
@ -1576,50 +1596,50 @@ export class CharacterSheet
|
|||
div("flex flex-col gap-4 py-1 w-60", [
|
||||
div("flex flex-col py-1 gap-4", [
|
||||
div("flex flex-row items-center justify-center gap-4", [
|
||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', label: 'Compétences', class: 'h-4' }) ]),
|
||||
dom("div", { class: 'text-xl font-semibold', text: "Compétences" }),
|
||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
||||
]),
|
||||
|
||||
div("grid grid-cols-2 gap-2",
|
||||
Object.keys(character.abilities).map((ability) =>
|
||||
div("flex flex-row px-1 justify-between items-center", [
|
||||
span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate", abilityTexts[ability as Ability] || ability),
|
||||
proses('a', preview, [ span("text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline", abilityTexts[ability as Ability] || ability) ], { href: `regles/l'entrainement/competences#${abilityTexts[ability as Ability]}`, label: abilityTexts[ability as Ability], navigate: false }),
|
||||
span("font-bold text-base text-light-100 dark:text-dark-100", text(() => `+${character.abilities[ability as Ability] ?? 0}`)),
|
||||
])
|
||||
)
|
||||
),
|
||||
|
||||
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', label: 'Compétences', class: 'h-4' }) ]),
|
||||
dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }),
|
||||
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', label: 'Arme légère', }) : undefined,
|
||||
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet' }) : undefined,
|
||||
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle' }) : undefined,
|
||||
() => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard' }) : undefined,
|
||||
() => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', label: 'Arme improvisée' }) : undefined,
|
||||
() => character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', label: 'Arme lourde' }) : undefined,
|
||||
() => character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', label: 'Arme à deux mains' }) : undefined,
|
||||
() => character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', label: 'Arme maniable' }) : undefined,
|
||||
() => character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', label: 'Arme à projectiles' }) : undefined,
|
||||
() => character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', label: 'Arme longue' }) : undefined,
|
||||
() => character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier' }) : undefined,
|
||||
() => character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', label: 'Bouclier à deux mains' }) : undefined,
|
||||
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', label: 'Arme légère', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline', }) : undefined,
|
||||
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', label: 'Arme de jet', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', label: 'Arme naturelle', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', label: 'Arme standard', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', label: 'Arme improvisée', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', label: 'Arme lourde', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', label: 'Arme à deux mains', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', label: 'Arme maniable', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', label: 'Arme à projectiles', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', label: 'Arme longue', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', label: 'Bouclier', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', label: 'Bouclier à deux mains', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : 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', label: 'Armure légère' }) : undefined,
|
||||
() => character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', label: 'Armure standard' }) : undefined,
|
||||
() => character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', label: 'Armure lourde' }) : undefined,
|
||||
() => character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', label: 'Armure légère', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', label: 'Armure standard', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
() => character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', label: 'Armure lourde', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }) : undefined,
|
||||
]) : undefined,
|
||||
|
||||
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
||||
div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', text(() => character.spellranks.precision)) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', text(() => character.spellranks.knowledge)) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', text(() => character.spellranks.instinct)) ]),
|
||||
div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', text(() => character.spellranks.arts)) ]),
|
||||
() => character.spellranks.precision > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Précision') ], { href: 'regles/la-magie/magie#Les sorts de précision', label: 'Précision', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.precision)) ]) : undefined,
|
||||
() => character.spellranks.knowledge > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Savoir') ], { href: 'regles/la-magie/magie#Les sorts de savoir', label: 'Savoir', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.knowledge)) ]) : undefined,
|
||||
() => character.spellranks.instinct > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Instinct') ], { href: 'regles/la-magie/magie#Les sorts instinctif', label: 'Instinct', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.instinct)) ]) : undefined,
|
||||
() => character.spellranks.arts > 0 ? div('flex flex-row items-center gap-2', [ proses('a', preview, [ text('Oeuvres') ], { href: 'regles/annexes/œuvres', label: 'Oeuvres', class: 'text-sm text-light-70 dark:text-dark-70 max-w-20 truncate cursor-help decoration-dotted underline' }), span('font-bold', text(() => character.spellranks.arts)) ]) : undefined,
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
|
@ -1636,14 +1656,14 @@ export class CharacterSheet
|
|||
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: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
|
||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }) ]),
|
||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||
div('flex flex-row items-center gap-2', [ ...Array(3).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: '/ tour' }) ]),
|
||||
div('flex flex-row items-center gap-2', [ div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]),
|
||||
]),
|
||||
|
||||
div('flex flex-col gap-2', [
|
||||
div('flex flex-row flex-wrap gap-2', ["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 => proses('a', a, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60' }))),
|
||||
div('flex flex-col gap-2', { render: (e) => div('flex flex-col gap-1', [
|
||||
div('flex flex-row flex-wrap gap-2', ["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 => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
|
||||
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
|
||||
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.action[e]?.name }), config.action[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.action[e]?.cost?.toString() }), text(`point${config.action[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
||||
markdown(getText(config.action[e]?.description), undefined, { tags: { a: preview } }),
|
||||
]), list: character.lists.action }),
|
||||
|
|
@ -1651,14 +1671,14 @@ export class CharacterSheet
|
|||
]),
|
||||
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: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
|
||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }) ]),
|
||||
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
||||
div('flex flex-row items-center gap-2', [ ...Array(2).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: '/ tour' }) ]),
|
||||
div('flex flex-row items-center gap-2', [ div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5'), dom('span', { class: 'tracking-tight', text: '/ tour' }) ]),
|
||||
]),
|
||||
|
||||
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' }))),
|
||||
div('flex flex-col gap-2', { render: (e) => div('flex flex-col gap-1', [
|
||||
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 => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
|
||||
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
|
||||
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.reaction[e]?.name }), config.reaction[e]?.cost ? div('flex flex-row gap-1', [dom('span', { class: 'font-bold', text: config.reaction[e]?.cost?.toString() }), text(`point${config.reaction[e]?.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
||||
markdown(getText(config.reaction[e]?.description), undefined, { tags: { a: preview } }),
|
||||
]), list: character.lists.reaction }),
|
||||
|
|
@ -1666,13 +1686,13 @@ export class CharacterSheet
|
|||
]),
|
||||
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: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
|
||||
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }) ]),
|
||||
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' }))),
|
||||
div('flex flex-col gap-2', { render: e => div('flex flex-col gap-1', [
|
||||
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 => proses('a', preview, [ span('cursor-pointer text-sm decoration-dotted underline', e) ], { href: 'regles/le-combat/actions-en-combat#' + e, label: e, trigger: 'hover', navigate: false, class: 'text-light-60 dark:text-dark-60', lowers: false }))),
|
||||
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
|
||||
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.freeaction[e]?.name }) ]),
|
||||
markdown(getText(config.freeaction[e]?.description), undefined, { tags: { a: preview } }),
|
||||
]), list: character.lists.reaction })
|
||||
|
|
@ -1684,7 +1704,7 @@ export class CharacterSheet
|
|||
abilitiesTab(character: CompiledCharacter)
|
||||
{
|
||||
return [
|
||||
div('flex flex-col gap-2', { render: e => div('flex flex-col gap-1', [
|
||||
div('flex flex-col gap-2', { render: (e, _c) => _c ?? div('flex flex-col gap-1', [
|
||||
div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: config.passive[e]?.name }) ]),
|
||||
markdown(getText(config.passive[e]?.description), undefined, { tags: { a: preview } }),
|
||||
]), list: character.lists.passive }),
|
||||
|
|
@ -1693,36 +1713,57 @@ export class CharacterSheet
|
|||
spellTab(character: CompiledCharacter)
|
||||
{
|
||||
const preference = reactive({
|
||||
sort: localStorage.getItem('character-sort') ?? 'rank',
|
||||
} as { sort: 'rank' | 'type' | 'element' });
|
||||
sort: localStorage.getItem('character-sort') ?? 'rank-asc',
|
||||
} as { sort: `${'rank'|'type'|'element'|'cost'|'range'|'speed'}-${'asc'|'desc'}` | '' });
|
||||
|
||||
const sort = (spells: string[]) => {
|
||||
localStorage.setItem('character-sort', preference.sort);
|
||||
const _spells = Object.keys(config.spells);
|
||||
|
||||
spells.sort((a, b) => _spells.indexOf(a) - _spells.indexOf(b));
|
||||
switch(preference.sort)
|
||||
{
|
||||
case 'rank': return spells.sort((a, b) => ((config.spells[a] as SpellConfig)?.rank ?? 0) - ((config.spells[b] as SpellConfig)?.rank ?? 0) || SPELL_ELEMENTS.indexOf((config.spells[a] as SpellConfig)?.elements[0]!) - SPELL_ELEMENTS.indexOf((config.spells[b] as SpellConfig)?.elements[0]!));
|
||||
case 'type': return spells.sort((a, b) => (config.spells[a] as SpellConfig)?.type.localeCompare((config.spells[b] as SpellConfig)?.type ?? '') || ((config.spells[a] as SpellConfig)?.rank ?? 0) - ((config.spells[b] as SpellConfig)?.rank ?? 0));
|
||||
case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf((config.spells[a] as SpellConfig)?.elements[0]!) - SPELL_ELEMENTS.indexOf((config.spells[b] as SpellConfig)?.elements[0]!) || ((config.spells[a] as SpellConfig)?.rank ?? 0) - ((config.spells[b] as SpellConfig)?.rank ?? 0));
|
||||
case 'rank-asc': return spells.sort((a, b) => (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
|
||||
case 'type-asc': return spells.sort((a, b) => config.spells[a]?.type.localeCompare(config.spells[b]?.type ?? '') ?? 0);
|
||||
case 'element-asc': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!));
|
||||
case 'cost-asc': return spells.sort((a, b) => (config.spells[a]?.cost ?? 0) - (config.spells[b]?.cost ?? 0));
|
||||
case 'range-asc': return spells.sort((a, b) => (config.spells[a]?.range === 'personnal' ? -1 : config.spells[a]?.range ?? 0) - (config.spells[b]?.range === 'personnal' ? -1 : config.spells[b]?.range ?? 0));
|
||||
case 'speed-asc': return spells.sort((a, b) => (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0) - (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0));
|
||||
|
||||
case 'rank-desc': return spells.sort((b, a) => (config.spells[a]?.rank ?? 0) - (config.spells[b]?.rank ?? 0));
|
||||
case 'type-desc': return spells.sort((b, a) => config.spells[a]?.type.localeCompare(config.spells[b]?.type ?? '') ?? 0);
|
||||
case 'element-desc': return spells.sort((b, a) => SPELL_ELEMENTS.indexOf(config.spells[a]?.elements[0]!) - SPELL_ELEMENTS.indexOf(config.spells[b]?.elements[0]!));
|
||||
case 'cost-desc': return spells.sort((b, a) => (config.spells[a]?.cost ?? 0) - (config.spells[b]?.cost ?? 0));
|
||||
case 'range-desc': return spells.sort((b, a) => (config.spells[a]?.range === 'personnal' ? -1 : config.spells[a]?.range ?? 0) - (config.spells[b]?.range === 'personnal' ? -1 : config.spells[b]?.range ?? 0));
|
||||
case 'speed-desc': return spells.sort((b, a) => (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0) - (config.spells[a]?.speed === 'action' ? -2 : config.spells[a]?.speed === 'reaction' ? -1 : config.spells[a]?.speed ?? 0));
|
||||
|
||||
default: return spells;
|
||||
}
|
||||
};
|
||||
|
||||
const panel = this.spellPanel(character);
|
||||
|
||||
const sorter = function(this: HTMLElement) { return followermenu(this, [
|
||||
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'rank-asc' ? 'rank-desc' : preference.sort === 'rank-desc' ? '' : 'rank-asc') } }, [text('Rang'), () => preference.sort.startsWith('rank') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
|
||||
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'type-asc' ? 'type-desc' : preference.sort === 'type-desc' ? '' : 'type-asc') } }, [text('Type'), () => preference.sort.startsWith('type') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
|
||||
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'element-asc' ? 'element-desc' : preference.sort === 'element-desc' ? '' : 'element-asc') } }, [text('Element'), () => preference.sort.startsWith('element') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
|
||||
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'cost-asc' ? 'cost-desc' : preference.sort === 'cost-desc' ? '' : 'cost-asc') } }, [text('Coût'), () => preference.sort.startsWith('cost') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
|
||||
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'range-asc' ? 'range-desc' : preference.sort === 'range-desc' ? '' : 'range-asc') } }, [text('Portée'), () => preference.sort.startsWith('range') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
|
||||
() => dom('div', { class: 'flex flex-row justify-between items-center gap-4 py-1 px-2 hover:bg-light-40 dark:hover:bg-dark-40 cursor-pointer', listeners: { click: () => preference.sort = (preference.sort === 'speed-asc' ? 'speed-desc' : preference.sort === 'speed-desc' ? '' : 'speed-asc') } }, [text('Incantation'), () => preference.sort.startsWith('speed') ? icon(preference.sort.endsWith('asc') ? 'ph:sort-ascending' : 'ph:sort-descending', { width: 16, height: 16 }) : undefined ]),
|
||||
], { class: 'text-light-100 dark:text-dark-100 w-32', offset: 8, placement: 'bottom-end', arrow: true });
|
||||
}
|
||||
|
||||
return [
|
||||
div('flex flex-col gap-2', [
|
||||
div('flex flex-row justify-between items-center', [
|
||||
div('flex flex-row gap-2 items-center', [
|
||||
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
|
||||
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: preference.sort as 'rank' | 'type' | 'element', class: { option: 'px-2 py-1 text-sm' }, onChange: (v) => { preference.sort = v; } }),
|
||||
]),
|
||||
div('flex flex-col gap-2 h-full', [
|
||||
div('flex flex-row justify-end items-center', [
|
||||
div('flex flex-row gap-2 items-center', [
|
||||
dom('span', { class: () => ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length + (character.lists.spells?.length ?? 0) !== character.spellslots }], text: () => `${character.variables.spells.length + (character.lists.spells?.length ?? 0)}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', character.variables.spells.length + (character.lists.spells?.length ?? 0) > 1 ? 's' : '') }),
|
||||
button(text('Modifier'), () => panel.show(), 'py-1 px-4'),
|
||||
tooltip(button(icon('ph:arrows-down-up', { width: 16, height: 16 }), sorter, 'p-1'), 'Trier par', 'right')
|
||||
])
|
||||
]),
|
||||
div('flex flex-col gap-2', { render: e => {
|
||||
div('flex flex-col gap-2 overflow-auto', { render: (e, _c) => {
|
||||
if(_c) return _c;
|
||||
|
||||
const spell = config.spells[e] as SpellConfig | undefined;
|
||||
|
||||
if(!spell)
|
||||
|
|
@ -1742,23 +1783,47 @@ export class CharacterSheet
|
|||
}
|
||||
spellPanel(character: CompiledCharacter)
|
||||
{
|
||||
const availableSpells = Object.values(config.spells).filter(spell => {
|
||||
if(spell.type === 'arts') return false;
|
||||
if(spell.rank === 4) return false;
|
||||
if(character.spellranks[spell.type] < spell.rank) return false;
|
||||
return true;
|
||||
}) as SpellConfig[];
|
||||
const spells = character.variables.spells;
|
||||
const filters = reactive<{ tag: Array<string>, rank: Array<SpellConfig['rank']>, type: Array<SpellConfig['type']>, element: Array<SpellConfig['elements'][number]>, cost: { min: number, max: number }, range: Array<SpellConfig['range']>, speed: Array<SpellConfig['speed']> }>({
|
||||
tag: [],
|
||||
type: [],
|
||||
rank: [],
|
||||
element: [],
|
||||
cost: { min: 0, max: Infinity },
|
||||
range: [],
|
||||
speed: [],
|
||||
});
|
||||
|
||||
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", [
|
||||
div("flex flex-row justify-between items-center", [
|
||||
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' }, [ text(() => `${spells.length + (character.lists.spells?.length ?? 0)}/${character.spellslots} sort(s) maitrisé(s)`.replaceAll('(s)', spells.length + (character.lists.spells?.length ?? 0) > 1 ? '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', { list: availableSpells, render: (spell) => foldable(() => [
|
||||
div('flex flex-row gap-2', [
|
||||
div('flex flex-col gap-1 items-center', [ text('Tags'), multiselect<typeof filters.tag[number]>([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { defaultValue: filters.tag, change: v => filters.tag = v, class: { container: 'w-32 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
|
||||
div('flex flex-col gap-1 items-center', [ text('Types'), multiselect<typeof filters.type[number]>(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { defaultValue: filters.type, change: v => filters.type = v, class: { container: 'w-28 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
|
||||
div('flex flex-col gap-1 items-center', [ text('Rangs'), multiselect<typeof filters.rank[number]>([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }], { defaultValue: filters.rank, change: v => filters.rank = v, class: { container: 'w-24 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
|
||||
div('flex flex-col gap-1 items-center', [ text('Elements'), multiselect<typeof filters.element[number]>(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { defaultValue: filters.element, change: v => filters.element = v, class: { container: 'w-28 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
|
||||
div('flex flex-col gap-1 items-center', [ text('Portée'), multiselect<typeof filters.range[number]>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { defaultValue: filters.range, change: v => filters.range = v, class: { container: 'w-28 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
|
||||
div('flex flex-col gap-1 items-center', [ text('Incantation'), multiselect<typeof filters.speed[number]>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { defaultValue: filters.speed, change: v => filters.speed = v, class: { container: 'w-32 !mx-0 text-xs', option: 'text-sm p-1' } }) ]),
|
||||
]),
|
||||
div('flex flex-col divide-y *:py-2 -my-2 overflow-y-auto', { list: () => Object.values(config.spells).filter(spell => {
|
||||
//if(spells.includes(spell.id)) return true;
|
||||
|
||||
if(character.spellranks[spell.type] < spell.rank) return false;
|
||||
if(filters.cost.min > spell.cost || spell.cost > filters.cost.max) return false;
|
||||
if(filters.element.length > 0 && !filters.element.some(e => spell.elements.includes(e))) return false;
|
||||
if(filters.range.length > 0 && !filters.range.includes(spell.range)) return false;
|
||||
if(filters.rank.length > 0 && !filters.rank.includes(spell.rank)) return false;
|
||||
if(filters.type.length > 0 && !filters.type.includes(spell.type)) return false;
|
||||
if(filters.speed.length > 0 && !filters.speed.includes(spell.speed)) return false;
|
||||
if(filters.tag.length > 0 && !filters.tag.some(e => spell.tags?.includes(e))) return false;
|
||||
|
||||
return true;
|
||||
}) as SpellConfig[], render: (spell, _c) => _c ?? foldable(() => [
|
||||
markdown(spell.description),
|
||||
], [ div("flex flex-row justify-between gap-2", [
|
||||
dom("span", { class: "text-lg font-bold", text: spell.name }),
|
||||
|
|
@ -1826,7 +1891,9 @@ export class CharacterSheet
|
|||
button(text('Modifier'), () => panel.show(), 'py-1 px-4'),
|
||||
]),
|
||||
]),
|
||||
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: character.variables.items, render: e => {
|
||||
div('flex flex-col flex-1 divide-y divide-light-35 dark:divide-dark-35', { list: character.variables.items, render: (e, _c) => {
|
||||
if(_c) return _c;
|
||||
|
||||
const item = config.items[e.id];
|
||||
|
||||
if(!item) return;
|
||||
|
|
@ -1836,7 +1903,8 @@ export class CharacterSheet
|
|||
const price = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.price }], [ icon('ph:coin', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.price }), () => item.price ? `${item.price * e.amount}` : '-') ]);
|
||||
const weight = div(() => ['flex flex-row min-w-16 gap-2 justify-between items-center px-2', { 'cursor-help': e.amount > 1 && !!item.weight }], [ icon('mdi:weight', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span(() => ({ 'underline decoration-1 decoration-dotted underline-offset-2': e.amount > 1 && !!item.weight }), () => item.weight ? `${item.weight * e.amount}` : '-') ]);
|
||||
return foldable(() => [
|
||||
markdown(getText(item.description)),
|
||||
markdown(getText(item.description)),
|
||||
() => e.enchantments && e.enchantments.length === 0 ? undefined : div('flex flex-row gap-1', { list: () => e.enchantments!.map(e => config.enchantments[e]).filter(e => !!e), render: (e, _c) => _c ?? floater(div('flex flex-row gap-2 border border-light-35 dark:border-dark-35 bg-light-15 dark:bg-dark-15 px-2 rounded-full py-px bg-light-cyan dark:bg-dark-cyan bg-opacity-20 dark:bg-opacity-20', [ span('text-sm font-semibold tracking-thigh', e.name), div('flex flex-row gap-1 items-center', [icon('game-icons:bolt-drop', { width: 12, height: 12 }), span('text-sm font-light', e.power)]) ]), () => [markdown(getText(e.description), undefined, { tags: { a: preview } })], { class: 'max-w-96 max-h-48 p-2', position: "right" }) }),
|
||||
div('flex flex-row justify-center gap-1', [
|
||||
this.character?.character.campaign ? button(text('Partager'), () => {
|
||||
|
||||
|
|
@ -1860,20 +1928,20 @@ export class CharacterSheet
|
|||
|
||||
this.character?.saveVariables();
|
||||
}, 'p-1'),
|
||||
button(text("Enchanter"), () => {
|
||||
() => !item.capacity ? undefined : button(text("Enchanter"), () => {
|
||||
enchant.show(e);
|
||||
}, 'px-2 text-sm h-5 box-content'),
|
||||
]) ], [div('flex flex-row justify-between', [
|
||||
])
|
||||
], [ div('flex flex-row justify-between', [
|
||||
div('flex flex-row items-center gap-4', [
|
||||
item.equippable ? checkbox({ defaultValue: e.equipped, change: v => {
|
||||
e.equipped = v;
|
||||
|
||||
//TODO: Toggle effect and enchants
|
||||
|
||||
|
||||
this.character?.saveVariables();
|
||||
this.character?.update(e);
|
||||
}, class: { container: '!w-5 !h-5' } }) : undefined,
|
||||
div('flex flex-row items-center gap-4', [ span([colorByRarity[item.rarity], 'text-lg'], item.name), div('flex flex-row gap-2 text-light-60 dark:text-dark-60 text-sm italic', subnameFactory(item).map(e => span('', e))) ]),
|
||||
item.category === 'armor' ? div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:shoulder-armor', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => `${item.health + ((e.state as ArmorState)?.health ?? 0) - ((e.state as ArmorState)?.loss ?? 0)}/${item.health + ((e.state as ArmorState)?.health ?? 0)} (${[item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0) > 0 ? '-' + (item.absorb.static + ((e.state as ArmorState).absorb?.flat ?? 0)) : undefined, item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0) > 0 ? '-' + (item.absorb.percent + ((e.state as ArmorState).absorb?.percent ?? 0)) + '%' : undefined].filter(e => !!e).join('/')})`) ]) :
|
||||
item.category === 'weapon' ? div('flex flex-row gap-2 items-center text-sm', [ icon('game-icons:broadsword', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }), span('italic', () => `${item.damage.value} ${(e.state as WeaponState)?.attack ? '+' + (e.state as WeaponState).attack : ''}`), proses('a', preview, [ text(damageTypeTexts[item.damage.type].toLowerCase()) ], { href: `regles/le-combat/les-types-de-degats#${damageTypeTexts[item.damage.type]}`, label: damageTypeTexts[item.damage.type], navigate: false }) ]) :
|
||||
undefined
|
||||
]),
|
||||
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
|
||||
e.amount > 1 && !!item.price ? tooltip(price, `Prix unitaire: ${item.price}`, 'bottom') : price,
|
||||
|
|
@ -1916,7 +1984,8 @@ export class CharacterSheet
|
|||
(filters.category.length === 0 || filters.category.includes(item.category)) &&
|
||||
(filters.rarity.length === 0 || filters.rarity.includes(item.rarity)) &&
|
||||
(filters.name === '' || item.name.toLowerCase().includes(filters.name.toLowerCase()))
|
||||
), render: (e) => {
|
||||
), render: (e, _c) => {
|
||||
if(_c) return _c;
|
||||
const item = config.items[e.id];
|
||||
|
||||
if(!item)
|
||||
|
|
@ -1988,7 +2057,7 @@ export class CharacterSheet
|
|||
}, "p-1"), "Fermer", "left")
|
||||
])
|
||||
]),
|
||||
div('grid grid-cols-1 -my-2 overflow-y-auto gap-1', { list: () => Object.values(config.enchantments).filter(e => restrict(e, current.item?.id)), render: (enchant) => foldable(() => [ markdown(getText(enchant.description)) ], [div('flex flex-row justify-between', [
|
||||
div('grid grid-cols-1 -my-2 overflow-y-auto gap-1', { list: () => Object.values(config.enchantments).filter(e => restrict(e, current.item?.id)), render: (enchant, _c) => _c ?? foldable(() => [ markdown(getText(enchant.description)) ], [div('flex flex-row justify-between', [
|
||||
div('flex flex-row items-center gap-4', [ span('text-lg', enchant.name) ]),
|
||||
div('flex flex-row items-center divide-x divide-light-50 dark:divide-dark-50 divide-dashed px-2', [
|
||||
span('italic text-sm', `Puissance magique: ${enchant.power}`),
|
||||
|
|
|
|||
|
|
@ -696,7 +696,7 @@ export function floater(container: RedrawableHTML, content: NodeChildren | (() =
|
|||
});
|
||||
minimized = false;
|
||||
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,
|
||||
div('group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
|
||||
div(['group-data-[minimized]:hidden h-full group-data-[pinned]:h-[calc(100%-21px)] w-full group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', settings?.class], typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex group-data-[minimized]:hidden absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ])
|
||||
],
|
||||
viewport,
|
||||
events: events
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ function append(dom: Element, children: Node | Node[] | undefined)
|
|||
|
||||
export const cancelPropagation = (e: Event) => e.stopImmediatePropagation();
|
||||
export function dom<T extends keyof HTMLElementTagNameMap>(tag: T, properties?: NodeProperties, children?: NodeChildren): HTMLElementTagNameMap[T];
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
|
||||
export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T, properties?: NodeProperties, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap[T] & { array?: DOMList<U> }
|
||||
{
|
||||
const element = document.createElement(tag) as HTMLElementTagNameMap[T] & { array?: DOMList<U> };
|
||||
const _cache = new Map<U, Node | Node[] | undefined>();
|
||||
|
|
@ -51,11 +52,14 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
|
|||
{
|
||||
if(Array.isArray(children))
|
||||
{
|
||||
for(const c of children)
|
||||
{
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
child && element.appendChild(child);
|
||||
}
|
||||
reactivity(children, (_children) => {
|
||||
element.replaceChildren();
|
||||
for(const c of _children)
|
||||
{
|
||||
const child = typeof c === 'function' ? c() : c;
|
||||
child && element.appendChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(children.list !== undefined)
|
||||
{
|
||||
|
|
@ -80,12 +84,8 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
|
|||
else
|
||||
{
|
||||
list.forEach(e => {
|
||||
let child = _cache.get(e);
|
||||
if(!child)
|
||||
{
|
||||
child = raw(children.render(e));
|
||||
_cache.set(e, child);
|
||||
}
|
||||
const child = raw(children.render(e, _cache.get(e)));
|
||||
_cache.set(e, child);
|
||||
append(element, child);
|
||||
});
|
||||
}
|
||||
|
|
@ -144,10 +144,9 @@ export function dom<T extends keyof HTMLElementTagNameMap, U extends any>(tag: T
|
|||
return element;
|
||||
}
|
||||
export function div(cls?: Reactive<Class>, children?: NodeChildren): HTMLElementTagNameMap['div']
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array: DOMList<U> }
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: NodeChildren | { render: (data: U) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array?: DOMList<U> }
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array: DOMList<U> }
|
||||
export function div<U extends any>(cls?: Reactive<Class>, children?: NodeChildren | { render: (data: U, cache?: Node | Node[]) => Node | Node[], list?: Reactive<Array<U>>, fallback?: () => Node | Node[] }): HTMLElementTagNameMap['div'] & { array?: DOMList<U> }
|
||||
{
|
||||
//@ts-expect-error wtf is wrong here ???
|
||||
return dom("div", { class: cls }, children);
|
||||
}
|
||||
export function span(cls?: Reactive<Class>, text?: Reactive<string | number | Text>): HTMLElementTagNameMap['span']
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, i18nID, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponType } from "~/types/character";
|
||||
import type { Ability, AspectConfig, CharacterConfig, CommonItemConfig, DamageType, Feature, FeatureChoice, FeatureEquipment, FeatureItem, FeatureList, FeatureValue, ItemConfig, Level, MainStat, RaceConfig, Resistance, SpellConfig, TrainingLevel, WeaponType } from "~/types/character";
|
||||
import { div, dom, icon, span, text, type NodeChildren, type RedrawableHTML } from "#shared/dom.util";
|
||||
import { MarkdownEditor } from "#shared/editor.util";
|
||||
import { preview } from "#shared/proses";
|
||||
|
|
@ -10,6 +10,7 @@ import { getID } from "#shared/general.util";
|
|||
import markdown, { markdownReference, renderMDAsText } from "#shared/markdown.util";
|
||||
import { Tree } from "#shared/tree";
|
||||
import { getText, setText } from "#shared/i18n";
|
||||
import { reactive } from "./reactive";
|
||||
|
||||
type Category = ItemConfig['category'];
|
||||
type Rarity = ItemConfig['rarity'];
|
||||
|
|
@ -211,18 +212,64 @@ export class HomebrewBuilder
|
|||
}
|
||||
spells()
|
||||
{
|
||||
const spellTagTexts = {
|
||||
'damage': 'Dégâts',
|
||||
'buff': 'Buff',
|
||||
'debuff': 'Débuff',
|
||||
'support': 'Support',
|
||||
'tank': 'Tank',
|
||||
'movement': 'Mouvement',
|
||||
'utilitary': 'Utilitaire',
|
||||
} as Record<string, string>;
|
||||
|
||||
const editing = reactive({
|
||||
id: '',
|
||||
});
|
||||
|
||||
const render = (spell: SpellConfig) => {
|
||||
return spell.type === 'arts' ? undefined : foldable([
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.filter(e => e !== 'arts').map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Portée'), select<'personnal' | number>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { change: (value) => spell.range = value, defaultValue: spell.range, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]),
|
||||
], [ div('gap-4 px-4 flex', [ input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }), input('text', { input: (value) => { spell.description = value }, defaultValue: spell.description, class: '!m-0 w-full' }),div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ]) ]) ], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
|
||||
return foldable(() => [
|
||||
markdown(spell.description, undefined, { tags: { a: preview } }),
|
||||
], [
|
||||
div('gap-4 px-4 flex', [
|
||||
input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }),
|
||||
div('flex flex-1 flex-row gap-2 items-center', [
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`Rang ${spell.rank}`) ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spellTypeTexts[spell.type]) ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${spell.cost} mana`) ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.speed === 'action' ? 'Action' : spell.speed === 'reaction' ? 'Réaction' : `${spell.speed} minutes`) ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${elementTexts[spell.elements[0]!].text}${spell.elements.length > 1 ? `+${spell.elements.length - 1}` : ''}`) ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.range === 'personnal' ? 'Personnel' : spell.range === 0 ? 'Toucher' : `${spell.range} cases`) ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(`${spell.tags && spell.tags.length > 0 ? spellTagTexts[spell.tags[0]!] : ''}${spell.tags && spell.tags.length > 1 ? `+${spell.tags.length - 1}` : ''}`) ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text(spell.concentration ? 'Concentration' : '') ]),
|
||||
]),
|
||||
div('flex flex-row justify-center gap-2', [ button(icon('radix-icons:pencil-1'), () => editing.id = spell.id, 'p-1'), button(icon('radix-icons:trash'), () => remove(spell), 'p-1') ])
|
||||
])
|
||||
], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
|
||||
}
|
||||
|
||||
const edit = (spell: SpellConfig) => {
|
||||
MarkdownEditor.singleton.onChange = v => {};
|
||||
MarkdownEditor.singleton.content = spell.description;
|
||||
return foldable([
|
||||
MarkdownEditor.singleton.dom
|
||||
], [
|
||||
div('gap-4 px-4 flex', [
|
||||
input('text', { input: (value) => { spell.name = value }, defaultValue: spell.name, class: '!m-0 w-64' }),
|
||||
div('flex flex-row gap-2 items-center', [
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Rang'), select([{ text: 'Rang 1', value: 1 }, { text: 'Rang 2', value: 2 }, { text: 'Rang 3', value: 3 }, { text: 'Spécial', value: 4 }], { change: (value: 1 | 2 | 3 | 4) => spell.rank = value, defaultValue: spell.rank, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Type'), select(SPELL_TYPES.map(f => ({ text: spellTypeTexts[f], value: f })), { change: (value) => spell.type = value, defaultValue: spell.type, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Coût'), numberpicker({ defaultValue: spell.cost, input: (value) => spell.cost = value, class: '!m-0 w-full' }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Incantation'), select<'action' | 'reaction' | number>([{ text: 'Action', value: 'action' }, { text: 'Reaction', value: 'reaction' }, { text: '1 minute', value: 1 }, { text: '10 minutes', value: 10 }], { change: (value) => spell.speed = value, defaultValue: spell.speed, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Elements'), multiselect(SPELL_ELEMENTS.map(f => ({ text: elementTexts[f].text, value: f })), { change: (value) => spell.elements = value, defaultValue: spell.elements, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Portée'), select<'personnal' | number>([{ text: 'Toucher', value: 0 }, { text: 'Personnel', value: 'personnal' }, { text: '3 cases', value: 3 }, { text: '6 cases', value: 6 }, { text: '9 cases', value: 9 }, { text: '12 cases', value: 12 }, { text: '18 cases', value: 18 }], { change: (value) => spell.range = value, defaultValue: spell.range, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Tags'), multiselect([{ text: 'Dégâts', value: 'damage' }, { text: 'Buff', value: 'buff' }, { text: 'Debuff', value: 'debuff' }, { text: 'Support', value: 'support' }, { text: 'Tank', value: 'tank' }, { text: 'Mouvement', value: 'movement' }, { text: 'Utilitaire', value: 'utilitary' }], { change: (value) => spell.tags = value, defaultValue: spell.tags, class: { container: '!m-0 !h-9 w-full' } }), ]),
|
||||
dom('label', { class: 'flex flex-col items-center justify-start gap-2 flex-1 *:text-center' }, [ text('Concentration'), toggle({ change: (value) => spell.concentration = value, defaultValue: spell.concentration, class: { container: '!m-0 !flex-none' } }), ]),
|
||||
]),
|
||||
div('flex flex-row gap-2', [ ])
|
||||
])
|
||||
], { class: { container: 'border-light-35 dark:border-dark-35 py-1', content: 'gap-2 px-4 py-1 flex items-center *:flex-1' }, open: false });
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
const id = getID();
|
||||
config.spells[id] = {
|
||||
|
|
@ -238,26 +285,16 @@ export class HomebrewBuilder
|
|||
range: 0,
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const element = redraw();
|
||||
content.parentElement?.replaceChild(element, content);
|
||||
content = element;
|
||||
};
|
||||
const remove = (spell: SpellConfig) => {
|
||||
confirm('Voulez vous vraiment supprimer ce sort ?').then(e => {
|
||||
if(e)
|
||||
{
|
||||
delete config.spells[spell.id];
|
||||
|
||||
const element = redraw();
|
||||
content.parentElement?.replaceChild(element, content);
|
||||
content = element;
|
||||
}
|
||||
});
|
||||
}
|
||||
const redraw = () => div('flex flex-col divide-y', Object.values(config.spells).map(render));
|
||||
let content = redraw();
|
||||
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), content ] ) ];
|
||||
return [ div('flex px-8 py-4 flex-col gap-4', [ div('flex flex-row-reverse', [ button(icon('radix-icons:plus'), add, 'p-1') ]), div('flex flex-col divide-y', { list: Object.values(config.spells), render: (e, _c) => editing.id === e.id ? edit(e) : _c ?? render(e)}) ] ) ];
|
||||
}
|
||||
actions()
|
||||
{
|
||||
|
|
@ -571,7 +608,7 @@ class FeatureEditor
|
|||
}
|
||||
else
|
||||
{
|
||||
list = (Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add') as FeatureList[]).map((e) => config[e.list][e.item]!).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('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(e.description))) ]) ]), value: e.id }));
|
||||
list = (Object.values(config.features).flatMap(e => e.effect).filter(e => e.category === 'list' && e.list === buffer.list && e.action === 'add') as FeatureList[]).map((e) => ({ text: config[e.list][e.item]!.name, render: () => div('flex flex-col', [ div('flex flex-row justify-between', [ dom('span', { text: config[e.list][e.item]!.name, class: 'font-bold' }) ]), div('text-sm text-light-70 dark:text-dark-70', [ text(renderMDAsText(getText(config[e.list === 'sickness' ? 'features' : e.list][e.item]?.description))) ]) ]), value: e.item }));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -868,7 +905,7 @@ const featureChoices: Option<Partial<FeatureOption>>[] = [
|
|||
{ text: 'Résistance > Psyché', effects: [{ category: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }] }
|
||||
]} as Partial<FeatureChoice>}
|
||||
] },
|
||||
{ text: 'Bonus à l\'attaque', value: RESISTANCES.map(e => ({ text: `Bonus > ${resistanceTexts[e]}`, value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) },
|
||||
{ text: 'Bonus à l\'attaque', value: RESISTANCES.map(e => ({ text: `Bonus > ${resistanceTexts[e]}`, value: { category: 'value', property: `bonus/resistance/${e}`, operation: 'add', value: 1 } })) },
|
||||
{ text: 'Magie', value: [
|
||||
{ text: 'Rang', value: [
|
||||
{ text: 'Rang > Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } },
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ export interface MDProperties
|
|||
class?: Class;
|
||||
style?: string | Record<string, string>;
|
||||
tags?: Record<string, Prose>;
|
||||
includeLowers?: boolean;
|
||||
}
|
||||
export function filterMarkdown(data: Root, filter: string)
|
||||
export function filterMarkdown(data: Root, filter: string, includeLowers: boolean = true)
|
||||
{
|
||||
const start = data?.children.findIndex(e => heading(e) && parseId(e.properties.id as string | undefined) === filter) ?? -1;
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ export function filterMarkdown(data: Root, filter: string)
|
|||
while(end < data.children.length)
|
||||
{
|
||||
end++;
|
||||
if(heading(data.children[end]) && headingRank(data.children[end]!)! <= rank)
|
||||
if(heading(data.children[end]) && (!includeLowers || headingRank(data.children[end]!)! <= rank))
|
||||
break;
|
||||
}
|
||||
return { ...data, children: data.children.slice(start, end) };
|
||||
|
|
@ -66,7 +67,7 @@ export function filterMarkdown(data: Root, filter: string)
|
|||
export function markdownReference(content: string, filter?: string, properties?: MDProperties)
|
||||
{
|
||||
const state = async('large', useMarkdown().parse(content).then(data => {
|
||||
if(filter) data = filterMarkdown(data, filter);
|
||||
if(filter) data = filterMarkdown(data, filter, properties?.includeLowers);
|
||||
|
||||
const el = dom('div', properties, data.children.map(e => renderContent(e, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags))));
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const a: Prose = {
|
|||
return !!overview ? floater(element, () => [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: 'w-full max-h-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', includeLowers: properties?.lowers });
|
||||
}
|
||||
if(_content?.type === 'canvas')
|
||||
{
|
||||
|
|
@ -46,7 +46,7 @@ export const a: Prose = {
|
|||
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
|
||||
}
|
||||
return div('');
|
||||
})).current], { events: { show: properties?.trigger !== 'click' ? ['mouseenter', 'mousemove', 'focus'] : ['click'], hide: properties?.trigger !== 'click' ? ['mouseleave', 'blur'] : ['click'] }, position: 'bottom-start', pinned: false, title: properties?.label, href: nav.href }) : element;
|
||||
})).current], { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', cover: 'all', events: { show: properties?.trigger !== 'click' ? ['mouseenter', 'mousemove', 'focus'] : ['click'], hide: properties?.trigger !== 'click' ? ['mouseleave', 'blur'] : ['click'] }, position: 'bottom-start', pinned: false, title: properties?.label, href: nav.href }) : element;
|
||||
}
|
||||
}
|
||||
export const preview: Prose = {
|
||||
|
|
@ -65,7 +65,7 @@ export const preview: Prose = {
|
|||
return !!overview ? floater(element, () => [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: 'w-full max-h-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', includeLowers: false });
|
||||
}
|
||||
if(_content?.type === 'canvas')
|
||||
{
|
||||
|
|
@ -74,11 +74,8 @@ export const preview: Prose = {
|
|||
return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]);
|
||||
}
|
||||
return div();
|
||||
})).current], { position: 'bottom-start', pinned: true,
|
||||
events: {
|
||||
show: ['click'],
|
||||
hide: ['click'],
|
||||
}, title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
|
||||
})).current], { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', delay: 500, cover: 'all', events: { show: properties?.trigger !== 'click' ? ['mouseenter', 'mousemove', 'focus'] : ['click'], hide: properties?.trigger !== 'click' ? ['mouseleave', 'blur'] : ['click'] }, position: 'bottom-start', pinned: false, title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
|
||||
//})).current], { class: 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]', position: 'bottom-start', pinned: false, delay: 0, cover: 'all', title: properties?.label, href: { name: 'explore-path', params: { path: overview.path }, hash: hash } }) : element;
|
||||
}
|
||||
}
|
||||
export const callout: Prose = {
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ const arraySubstitute = <any>{ // <-- <any> is required to allow __proto__ witho
|
|||
toSorted(comparer?: (a: unknown, b: unknown) => number) { return reactiveReadArray(this).toSorted(comparer) },
|
||||
toSpliced(...args: unknown[]) { return (reactiveReadArray(this).toSpliced as any)(...args) },
|
||||
unshift(...args: unknown[]) { return noTracking(this, 'unshift', args) },
|
||||
values() { return iterator(this, 'values', item => wrapReactive(item)) }, /* */
|
||||
values() { return iterator(this, 'values', item => wrapReactive(item)) },
|
||||
};
|
||||
|
||||
// Store object to proxy correspondance
|
||||
|
|
@ -228,9 +228,6 @@ export function reactive<T extends object>(obj: T | Proxy<T>): T | Proxy<T>
|
|||
if(_reactiveCache.has(obj))
|
||||
return _reactiveCache.get(obj)!;
|
||||
|
||||
const prototype = Object.getPrototypeOf(obj);
|
||||
const isArray = Array.isArray(obj);
|
||||
|
||||
const proxy = new Proxy<T>(obj, {
|
||||
get: (target, key, receiver) => {
|
||||
if(key === SYMBOLS.PROXY)
|
||||
|
|
|
|||
Loading…
Reference in New Issue