|
|
|
|
@@ -1,9 +1,9 @@
|
|
|
|
|
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
|
|
|
|
import type { Ability, Alignment, Character, CharacterConfig, CharacterVariables, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellConfig, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
|
|
|
|
import { z } from "zod/v4";
|
|
|
|
|
import characterConfig from '#shared/character-config.json';
|
|
|
|
|
import proses, { preview } from "#shared/proses";
|
|
|
|
|
import { button, buttongroup, foldable, input, loading, numberpicker, select, tabgroup, Toaster, toggle } from "#shared/components.util";
|
|
|
|
|
import { div, dom, icon, text } from "#shared/dom.util";
|
|
|
|
|
import { div, dom, icon, span, text } from "#shared/dom.util";
|
|
|
|
|
import { followermenu, fullblocker, tooltip } from "#shared/floating.util";
|
|
|
|
|
import { clamp } from "#shared/general.util";
|
|
|
|
|
import markdown from "#shared/markdown.util";
|
|
|
|
|
@@ -40,6 +40,7 @@ export const defaultCharacter: Character = {
|
|
|
|
|
items: [],
|
|
|
|
|
exhaustion: 0,
|
|
|
|
|
sickness: [],
|
|
|
|
|
poisons: [],
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
owner: -1,
|
|
|
|
|
@@ -113,7 +114,10 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|
|
|
|
magicelement: 0,
|
|
|
|
|
magicinstinct: 0,
|
|
|
|
|
},
|
|
|
|
|
bonus: {},
|
|
|
|
|
bonus: {
|
|
|
|
|
abilities: {},
|
|
|
|
|
defense: {},
|
|
|
|
|
},
|
|
|
|
|
resistance: {},
|
|
|
|
|
initiative: 0,
|
|
|
|
|
capacity: 0,
|
|
|
|
|
@@ -125,7 +129,7 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|
|
|
|
spells: [],
|
|
|
|
|
},
|
|
|
|
|
aspect: "",
|
|
|
|
|
notes: character.notes ?? "",
|
|
|
|
|
notes: Object.assign({ public: '', private: '' }, character.notes),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const mainStatTexts: Record<MainStat, string> = {
|
|
|
|
|
@@ -158,6 +162,10 @@ export const elementTexts: Record<SpellElement, { class: string, text: string }>
|
|
|
|
|
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
|
|
|
|
|
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
|
|
|
|
|
};
|
|
|
|
|
export const elementDom = (element: SpellElement) => dom("span", {
|
|
|
|
|
class: [`border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px`, elementTexts[element].class],
|
|
|
|
|
text: elementTexts[element].text
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export const alignmentTexts: Record<Alignment, string> = {
|
|
|
|
|
'loyal_good': 'Loyal bon',
|
|
|
|
|
@@ -205,6 +213,22 @@ export const resistanceTexts: Record<Resistance, string> = {
|
|
|
|
|
'instinct': 'Sorts d\'instinct',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const CharacterVariablesValidation = z.object({
|
|
|
|
|
health: z.number(),
|
|
|
|
|
mana: z.number(),
|
|
|
|
|
exhaustion: z.number(),
|
|
|
|
|
|
|
|
|
|
sickness: z.array(z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
state: z.number().min(1).max(7).or(z.literal(true)),
|
|
|
|
|
})),
|
|
|
|
|
poisons: z.array(z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
state: z.number().min(1).max(7).or(z.literal(true)),
|
|
|
|
|
})),
|
|
|
|
|
spells: z.array(z.string()),
|
|
|
|
|
equipment: z.array(z.string()),
|
|
|
|
|
});
|
|
|
|
|
export const CharacterValidation = z.object({
|
|
|
|
|
id: z.number(),
|
|
|
|
|
name: z.string(),
|
|
|
|
|
@@ -216,18 +240,7 @@ export const CharacterValidation = z.object({
|
|
|
|
|
leveling: z.record(z.enum(LEVELS.map(String)), z.number().optional()),
|
|
|
|
|
abilities: z.record(z.enum(ABILITIES), z.number().optional()),
|
|
|
|
|
choices: z.record(z.string(), z.array(z.number())),
|
|
|
|
|
variables: z.object({
|
|
|
|
|
health: z.number(),
|
|
|
|
|
mana: z.number(),
|
|
|
|
|
exhaustion: z.number(),
|
|
|
|
|
|
|
|
|
|
sickness: z.array(z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
state: z.number().min(1).max(7).or(z.literal(true)),
|
|
|
|
|
})),
|
|
|
|
|
spells: z.array(z.string()),
|
|
|
|
|
equipment: z.array(z.string()),
|
|
|
|
|
}),
|
|
|
|
|
variables: CharacterVariablesValidation,
|
|
|
|
|
owner: z.number(),
|
|
|
|
|
username: z.string().optional(),
|
|
|
|
|
visibility: z.enum(["public", "private"]),
|
|
|
|
|
@@ -241,6 +254,7 @@ export class CharacterCompiler
|
|
|
|
|
protected _character!: Character;
|
|
|
|
|
protected _result!: CompiledCharacter;
|
|
|
|
|
protected _buffer: Record<string, PropertySum> = {};
|
|
|
|
|
private _variableDirty: boolean = false;
|
|
|
|
|
|
|
|
|
|
constructor(character: Character)
|
|
|
|
|
{
|
|
|
|
|
@@ -299,6 +313,20 @@ export class CharacterCompiler
|
|
|
|
|
{
|
|
|
|
|
this._character.variables[prop] = value;
|
|
|
|
|
this._result.variables[prop] = value;
|
|
|
|
|
this._variableDirty = true;
|
|
|
|
|
}
|
|
|
|
|
saveVariables()
|
|
|
|
|
{
|
|
|
|
|
if(this._variableDirty)
|
|
|
|
|
{
|
|
|
|
|
this._variableDirty = false;
|
|
|
|
|
useRequestFetch()(`/api/character/${this.character.id}/variables`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: this._character.variables,
|
|
|
|
|
}).then(() => {}).catch(() => {
|
|
|
|
|
Toaster.add({ type: 'error', content: 'Impossible de mettre à jour les données', duration: 5000, timer: true });
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
protected add(feature?: string)
|
|
|
|
|
{
|
|
|
|
|
@@ -1142,10 +1170,13 @@ class AspectPicker extends BuilderTab
|
|
|
|
|
|
|
|
|
|
export class CharacterSheet
|
|
|
|
|
{
|
|
|
|
|
user: ComputedRef<User | null>;
|
|
|
|
|
character?: CharacterCompiler;
|
|
|
|
|
container: HTMLElement = div();
|
|
|
|
|
tabs?: HTMLDivElement & { refresh: () => void };
|
|
|
|
|
constructor(id: string, user: ComputedRef<User | null>)
|
|
|
|
|
{
|
|
|
|
|
this.user = user;
|
|
|
|
|
const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]);
|
|
|
|
|
this.container.replaceChildren(load);
|
|
|
|
|
useRequestFetch()(`/api/character/${id}`).then(character => {
|
|
|
|
|
@@ -1159,9 +1190,16 @@ export class CharacterSheet
|
|
|
|
|
this.render();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//ERROR
|
|
|
|
|
}
|
|
|
|
|
throw new Error();
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
this.container.replaceChildren(div('flex flex-col items-center justify-center flex-1 h-full gap-4', [
|
|
|
|
|
span('text-2xl font-bold tracking-wider', 'Personnage introuvable'),
|
|
|
|
|
span(undefined, 'Ce personnage n\'existe pas ou est privé.'),
|
|
|
|
|
div('flex flex-row gap-4 justify-center items-center', [
|
|
|
|
|
button(text('Personnages publics'), () => useRouter().push({ name: 'character-list' }), 'px-2 py-1'),
|
|
|
|
|
button(text('Créer un personange'), () => useRouter().push({ name: 'character-id-edit', params: { id: 'new' } }), 'px-2 py-1')
|
|
|
|
|
])
|
|
|
|
|
]))
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
render()
|
|
|
|
|
@@ -1170,7 +1208,22 @@ export class CharacterSheet
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const character = this.character.compiled;
|
|
|
|
|
console.log(character);
|
|
|
|
|
|
|
|
|
|
this.tabs = tabgroup([
|
|
|
|
|
{ id: 'actions', title: [ text('Actions') ], content: () => this.actionsTab(character) },
|
|
|
|
|
|
|
|
|
|
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) },
|
|
|
|
|
|
|
|
|
|
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
|
|
|
|
|
|
|
|
|
|
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
|
|
|
|
|
|
|
|
|
] },
|
|
|
|
|
|
|
|
|
|
{ id: 'notes', title: [ text('Notes') ], content: () => [
|
|
|
|
|
|
|
|
|
|
] },
|
|
|
|
|
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } });
|
|
|
|
|
this.container.replaceChildren(div('flex flex-col justify-center gap-1', [
|
|
|
|
|
div("flex flex-row gap-4 justify-between", [
|
|
|
|
|
div(),
|
|
|
|
|
@@ -1215,10 +1268,9 @@ export class CharacterSheet
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div("self-center", [
|
|
|
|
|
/* user && user.id === character.owner ?
|
|
|
|
|
button(icon("radix-icons:pencil-2"), () => {
|
|
|
|
|
}, "icon")
|
|
|
|
|
: div() */
|
|
|
|
|
this.user.value && this.user.value.id === character.owner ?
|
|
|
|
|
button(icon("radix-icons:pencil-2"), () => useRouter().push({ name: 'character-id-edit', params: { id: this.character?.character.id } }), "p-1")
|
|
|
|
|
: div()
|
|
|
|
|
])
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
@@ -1299,7 +1351,7 @@ export class CharacterSheet
|
|
|
|
|
div("flex flex-col gap-4 py-1 w-80", [
|
|
|
|
|
div("flex flex-col py-1 gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Compétences" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
@@ -1314,107 +1366,97 @@ export class CharacterSheet
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-xl font-semibold', text: "Maitrises" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 16, height: 16, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/l\'entrainement/competences', size: 'small', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50")
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
character.mastery.strength + character.mastery.dexterity > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
|
|
|
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères' }) : undefined,
|
|
|
|
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet' }) : undefined,
|
|
|
|
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles' }) : undefined,
|
|
|
|
|
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes' }) : undefined,
|
|
|
|
|
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées' }) : undefined,
|
|
|
|
|
character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes' }) : undefined,
|
|
|
|
|
character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains' }) : undefined,
|
|
|
|
|
character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables' }) : undefined,
|
|
|
|
|
character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles' }) : undefined,
|
|
|
|
|
character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues' }) : undefined,
|
|
|
|
|
character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers' }) : undefined,
|
|
|
|
|
character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains' }) : undefined,
|
|
|
|
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme légère') ], { href: 'regles/annexes/equipement#Les armes légères', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme de jet') ], { href: 'regles/annexes/equipement#Les armes de jet', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.strength + character.mastery.dexterity > 0 ? proses('a', preview, [ text('Arme naturelle') ], { href: 'regles/annexes/equipement#Les armes naturelles', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme standard') ], { href: 'regles/annexes/equipement#Les armes', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.strength > 1 ? proses('a', preview, [ text('Arme improvisée') ], { href: 'regles/annexes/equipement#Les armes improvisées', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.strength > 2 ? proses('a', preview, [ text('Arme lourde') ], { href: 'regles/annexes/equipement#Les armes lourdes', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.strength > 3 ? proses('a', preview, [ text('Arme à deux mains') ], { href: 'regles/annexes/equipement#Les armes à deux mains', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.dexterity > 0 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme maniable') ], { href: 'regles/annexes/equipement#Les armes maniables', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.dexterity > 1 && character.mastery.strength > 1 ? proses('a', preview, [ text('Arme à projectiles') ], { href: 'regles/annexes/equipement#Les armes à projectiles', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.dexterity > 1 && character.mastery.strength > 2 ? proses('a', preview, [ text('Arme longue') ], { href: 'regles/annexes/equipement#Les armes longues', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.shield > 0 ? proses('a', preview, [ text('Bouclier') ], { href: 'regles/annexes/equipement#Les boucliers', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.shield > 0 && character.mastery.strength > 3 ? proses('a', preview, [ text('Bouclier à deux mains') ], { href: 'regles/annexes/equipement#Les boucliers à deux mains', size: 'small' }) : undefined,
|
|
|
|
|
]) : undefined,
|
|
|
|
|
|
|
|
|
|
character.mastery.armor > 0 ? div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
|
|
|
|
character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères' }) : undefined,
|
|
|
|
|
character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures' }) : undefined,
|
|
|
|
|
character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes' }) : undefined,
|
|
|
|
|
character.mastery.armor > 0 ? proses('a', preview, [ text('Armure légère') ], { href: 'regles/annexes/equipement#Les armures légères', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.armor > 1 ? proses('a', preview, [ text('Armure standard') ], { href: 'regles/annexes/equipement#Les armures', size: 'small' }) : undefined,
|
|
|
|
|
character.mastery.armor > 2 ? proses('a', preview, [ text('Armure lourde') ], { href: 'regles/annexes/equipement#Les armures lourdes', size: 'small' }) : undefined,
|
|
|
|
|
]) : undefined,
|
|
|
|
|
|
|
|
|
|
div("grid grid-cols-2 gap-x-3 gap-y-1 text-sm", [
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Précision'), dom('span', { text: character.spellranks.precision.toString(), class: 'font-bold' }) ]),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Savoir'), dom('span', { text: character.spellranks.knowledge.toString(), class: 'font-bold' }) ]),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Instinct'), dom('span', { text: character.spellranks.instinct.toString(), class: 'font-bold' }) ]),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Oeuvres'), dom('span', { text: character.spellranks.arts.toString(), class: 'font-bold' }) ]),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Précision'), span('font-bold', character.spellranks.precision.toString()) ]),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Savoir'), span('font-bold', character.spellranks.knowledge.toString()) ]),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Instinct'), span('font-bold', character.spellranks.instinct.toString()) ]),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ text('Oeuvres'), span('font-bold', character.spellranks.arts.toString()) ]),
|
|
|
|
|
])
|
|
|
|
|
])
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div('border-l border-light-35 dark:border-dark-35'),
|
|
|
|
|
|
|
|
|
|
tabgroup([
|
|
|
|
|
{ id: 'actions', title: [ text('Actions') ], content: () => [
|
|
|
|
|
div('flex flex-col gap-8', [
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
|
|
|
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
|
|
|
|
|
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
|
|
|
|
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
|
|
|
])) ?? [])
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
|
|
|
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
|
|
|
|
|
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
|
|
|
|
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
|
|
|
])) ?? [])
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
|
|
|
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
|
|
|
|
|
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
|
|
|
|
|
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
|
|
|
])) ?? [])
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
] },
|
|
|
|
|
|
|
|
|
|
{ id: 'abilities', title: [ text('Aptitudes') ], content: () => this.abilitiesTab(character) },
|
|
|
|
|
|
|
|
|
|
{ id: 'spells', title: [ text('Sorts') ], content: () => this.spellTab(character) },
|
|
|
|
|
|
|
|
|
|
{ id: 'inventory', title: [ text('Inventaire') ], content: () => [
|
|
|
|
|
|
|
|
|
|
] },
|
|
|
|
|
|
|
|
|
|
{ id: 'notes', title: [ text('Notes') ], content: () => [
|
|
|
|
|
|
|
|
|
|
] },
|
|
|
|
|
], { focused: 'abilities', class: { container: 'flex-1 gap-4 px-4 w-[960px]' } }),
|
|
|
|
|
this.tabs,
|
|
|
|
|
])
|
|
|
|
|
]));
|
|
|
|
|
}
|
|
|
|
|
actionsTab(character: CompiledCharacter)
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
div('flex flex-col gap-8', [
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Actions', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ ...Array(character.action).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Attaquer", "Désarmer", "Saisir", "Faire chuter", "Déplacer", "Courir", "Pas de coté", "Charger", "Lancer un sort", "S'interposer", "Se transformer", "Utiliser un objet", "Anticiper une action", "Improviser"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
|
|
|
...(character.lists.action?.map(e => div('flex flex-col gap-1', [
|
|
|
|
|
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
|
|
|
|
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
|
|
|
])) ?? [])
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Réactions" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Réaction', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
|
|
|
div('flex flex-row items-center gap-2', [ ...Array(character.reaction).fill(undefined).map(e => div('border border-dashed border-light-50 dark:border-dark-50 w-5 h-5')), dom('span', { class: 'tracking-tight', text: '/ round' }) ]),
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Parer", "Esquiver", "Saisir une opportunité", "Prendre en tenaille", "Intercepter"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
|
|
|
...(character.lists.reaction?.map(e => div('flex flex-col gap-1', [
|
|
|
|
|
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`point${e.cost > 1 ? 's' : ''} d'action`)]) : undefined]),
|
|
|
|
|
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
|
|
|
])) ?? [])
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-4", [
|
|
|
|
|
div("flex flex-row items-center justify-center gap-2", [ dom("div", { class: 'text-lg font-semibold', text: "Actions libres" }), proses('a', preview, [ icon('radix-icons:question-mark-circled', { width: 12, height: 12, class: 'text-light-70 dark:text-dark-70' }) ], { href: 'regles/le-combat/actions-en-combat#Action libre', class: 'h-4' }) ]),
|
|
|
|
|
div("flex flex-1 border-t border-dashed border-light-50 dark:border-dark-50"),
|
|
|
|
|
]),
|
|
|
|
|
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row flex-wrap gap-2 text-light-60 dark:text-dark-60', ["Analyser une situation", "Communiquer", "Dégainer", "Attraper un objet"].map(e => dom('span', { text: e, class: 'cursor-pointer text-sm decoration-dotted underline' }))),
|
|
|
|
|
...(character.lists.freeaction?.map(e => div('flex flex-col gap-1', [
|
|
|
|
|
//div('flex flex-row justify-between', [dom('span', { class: 'text-lg', text: e.title }), e.cost ? div('flex flex-row', [dom('span', { class: 'font-bold', text: e.cost }), text(`action${e.cost > 1 ? 's' : ''} libre`)]) : undefined]),
|
|
|
|
|
markdown(getText(e), undefined, { tags: { a: preview } }),
|
|
|
|
|
])) ?? [])
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
]),
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
abilitiesTab(character: CompiledCharacter)
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
@@ -1428,23 +1470,45 @@ export class CharacterSheet
|
|
|
|
|
}
|
|
|
|
|
spellTab(character: CompiledCharacter)
|
|
|
|
|
{
|
|
|
|
|
let sortPreference = (localStorage.getItem('character-sort') ?? 'rank') as 'rank' | 'type' | 'element';
|
|
|
|
|
|
|
|
|
|
const sort = (spells: Array<{ id: string, spell?: SpellConfig, source: string }>) => {
|
|
|
|
|
spells = spells.filter(e => !!e.spell);
|
|
|
|
|
switch(sortPreference)
|
|
|
|
|
{
|
|
|
|
|
case 'rank': return spells.sort((a, b) => a.spell!.rank - b.spell!.rank || SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!));
|
|
|
|
|
case 'type': return spells.sort((a, b) => a.spell!.type.localeCompare(b.spell!.type) || a.spell!.rank - b.spell!.rank);
|
|
|
|
|
case 'element': return spells.sort((a, b) => SPELL_ELEMENTS.indexOf(a.spell!.elements[0]!) - SPELL_ELEMENTS.indexOf(b.spell!.elements[0]!) || a.spell!.rank - b.spell!.rank);
|
|
|
|
|
default: return spells;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const spells = sort([...(character.lists.spells ?? []).map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'feature' })), ...character.variables.spells.map(e => ({ id: e, spell: config.spells.find(_e => _e.id === e), source: 'player' }))]).map(e => ({...e, dom:
|
|
|
|
|
e.spell ? div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row items-center gap-4', [ dom('span', { class: 'font-semibold text-lg', text: e.spell.name ?? 'Inconnu' }), div('flex-1 border-b border-dashed border-light-50 dark:border-dark-50'), dom('span', { class: 'text-light-70 dark:text-dark-70', text: `${e.spell.cost ?? 0} mana` }) ]),
|
|
|
|
|
div('flex flex-row justify-between items-center gap-2 text-light-70 dark:text-dark-70', [
|
|
|
|
|
div('flex flex-row gap-2', [ span('flex flex-row', e.spell.rank === 4 ? 'Sort unique' : `Sort ${e.spell.type === 'instinct' ? 'd\'instinct' : e.spell.type === 'knowledge' ? 'de savoir' : 'de précision'} de rang ${e.spell.rank}`), ...(e.spell.elements ?? []).map(elementDom) ]),
|
|
|
|
|
div('flex flex-row gap-2', [ e.spell.concentration ? proses('a', preview, [span('italic text-sm', 'concentration')], { href: '' }) : undefined, span(undefined, typeof e.spell.speed === 'number' ? `${e.spell.speed} minute${e.spell.speed > 1 ? 's' : ''}` : e.spell.speed) ])
|
|
|
|
|
]),
|
|
|
|
|
div('flex flex-row ps-4 p-1 border-l-4 border-light-35 dark:border-dark-35', [ markdown(e.spell.effect) ]),
|
|
|
|
|
]) : undefined }));
|
|
|
|
|
return [
|
|
|
|
|
div('flex flex-col gap-2', [
|
|
|
|
|
div('flex flex-row justify-between items-center', [
|
|
|
|
|
div('flex flex-row gap-2 items-center', [
|
|
|
|
|
dom('span', { class: 'italic tracking-tight text-sm', text: 'Trier par' }),
|
|
|
|
|
buttongroup([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: 'rank', class: { option: 'px-2 py-1 text-sm' } }),
|
|
|
|
|
buttongroup<'rank' | 'type' | 'element'>([{ text: 'Rang', value: 'rank' }, { text: 'Type', value: 'type' }, { text: 'Element', value: 'element' }], { value: sortPreference, class: { option: 'px-2 py-1 text-sm' }, onChange: (value) => { localStorage.setItem('character-sort', value); sortPreference = value; this.tabs?.refresh(); } }),
|
|
|
|
|
]),
|
|
|
|
|
div('flex flex-row gap-2 items-center', [
|
|
|
|
|
dom('span', { class: ['italic text-sm', { 'text-light-red dark:text-dark-red': character.variables.spells.length !== character.spellslots }], text: `${character.variables.spells.length}/${character.spellslots} sort${character.variables.spells.length > 1 ? 's' : ''} maitrisé${character.variables.spells.length > 1 ? 's' : ''}` }),
|
|
|
|
|
button(text('Modifier'), () => this.spellPanel(character, spells), 'py-1 px-4'),
|
|
|
|
|
])
|
|
|
|
|
])
|
|
|
|
|
]),
|
|
|
|
|
div('flex flex-col gap-2', spells.map(e => e.dom))
|
|
|
|
|
])
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
spellPanel()
|
|
|
|
|
spellPanel(character: CompiledCharacter, spelllist: Array<{ id: string, spell?: SpellConfig, source: string }>)
|
|
|
|
|
{
|
|
|
|
|
if(!this.character)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const character = this.character.compiled;
|
|
|
|
|
const availableSpells = Object.values(config.spells).filter(spell => {
|
|
|
|
|
if (spell.rank === 4) return false;
|
|
|
|
|
if (character.spellranks[spell.type] < spell.rank) return false;
|
|
|
|
|
@@ -1465,17 +1529,17 @@ export class CharacterSheet
|
|
|
|
|
const toggleText = text(state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter'), toggleButton = button(toggleText, () => {
|
|
|
|
|
if(state === 'choosen')
|
|
|
|
|
{
|
|
|
|
|
//this.character.variable('spells', character.variables.spells.filter(e => e !== spell.id)); //TO REWORK
|
|
|
|
|
this.character!.variable('spells', character.variables.spells.filter(e => e !== spell.id));
|
|
|
|
|
state = 'empty';
|
|
|
|
|
}
|
|
|
|
|
else if(state === 'empty')
|
|
|
|
|
{
|
|
|
|
|
//this.character.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
|
|
|
|
|
this.character!.variable('spells', [...character.variables.spells, spell.id]); //TO REWORK
|
|
|
|
|
state = 'choosen';
|
|
|
|
|
}
|
|
|
|
|
//character = compiler.compiled; //TO REWORK
|
|
|
|
|
toggleText.textContent = state === 'choosen' ? 'Supprimer' : state === 'given' ? 'Inné' : 'Ajouter';
|
|
|
|
|
textAmount.textContent = character.variables.spells.length.toString();
|
|
|
|
|
this.tabs?.refresh();
|
|
|
|
|
}, "px-2 py-1 text-sm font-normal");
|
|
|
|
|
toggleButton.disabled = state === 'given';
|
|
|
|
|
return foldable(() => [
|
|
|
|
|
@@ -1507,7 +1571,7 @@ export class CharacterSheet
|
|
|
|
|
]) ], { open: false, class: { container: "px-2 flex flex-col border-light-35 dark:border-dark-35", content: 'py-2' } });
|
|
|
|
|
}))
|
|
|
|
|
]);
|
|
|
|
|
const blocker = fullblocker([ container ], { closeWhenOutside: true });
|
|
|
|
|
const blocker = fullblocker([ container ], { closeWhenOutside: true, onClose: () => this.character?.saveVariables() });
|
|
|
|
|
setTimeout(() => container.setAttribute('data-state', 'active'), 1);
|
|
|
|
|
}
|
|
|
|
|
}
|