Training viewer and properties manager preparation

This commit is contained in:
Clément Pons 2025-06-04 17:42:47 +02:00
parent 42915d699f
commit 218b68db60
10 changed files with 123 additions and 3991 deletions

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { MAIN_STATS, mainStatTexts, type CharacterConfig } from '~/types/character';
import PreviewA from './prose/PreviewA.vue';
const { config } = defineProps<{
config: CharacterConfig
}>();
const position = ref(0);
</script>
<template>
<TabsRoot class="flex flex-1 flex-row justify-start items-stretch w-full" :default-value="MAIN_STATS[0]">
<TabsList class="flex flex-col gap-3 relative">
<TabsTrigger v-for="(stat, i) of MAIN_STATS" :value="stat" class="block w-2.5 h-2.5 m-px outline outline-1 outline-transparent hover:outline-light-70 dark:hover:outline-dark-70 rounded-full bg-light-40 dark:bg-dark-40 cursor-pointer" @click="position = i"></TabsTrigger>
<!-- <TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator> -->
<span :style="{ 'top': position * 1.5 + 'em' }" :data-text="mainStatTexts[MAIN_STATS[position]]" class="rounded-full w-3 h-3 bg-accent-blue absolute transition-[top]
after:content-[attr(data-text)] after:absolute after:-top-2 after:left-4 after:p-px after:bg-light-0 dark:after:bg-dark-0"></span>
</TabsList>
<TabsContent v-for="(stat) of MAIN_STATS" :value="stat" class="flex-1">
<div class="flex flex-row overflow-x-auto w-full flex-nowrap gap-4">
<div class="w-96 flex flex-col gap-4 justify-between" v-for="(level) of config.training[stat]">
<div class="border border-light-35 dark:border-dark-35 px-3 py-1" v-for="(option) of level"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
</div>
</div>
</TabsContent>
</TabsRoot>
</template>

BIN
db.sqlite

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -39,7 +39,7 @@
</template>
</div>
</div>
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
<div class="flex flex-1 flex-row relative max-w-[100vw] h-screen overflow-hidden">
<CollapsibleContent asChild forceMount>
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden">

View File

@ -0,0 +1,78 @@
<script setup lang="ts">
import characterConfig from '#shared/character-config.json';
import { Icon } from '@iconify/vue/dist/iconify.js';
import PreviewA from '~/components/prose/PreviewA.vue';
import { mainStatTexts, MAIN_STATS, type CharacterConfig } from '~/types/character';
//@ts-ignore
const config = ref<CharacterConfig>(characterConfig);
const trainingTab = ref(0);
function copy()
{
navigator.clipboard.writeText(JSON.stringify(config.value));
}
</script>
<template>
<Head>
<Title>d[any] - Edition de données</Title>
</Head>
<TabsRoot class="flex flex-1 flex-col justify-start items-center gap-4 px-8 w-full" default-value="training">
<TabsList class="flex flex-row gap-4 self-center relative px-4">
<TabsIndicator class="absolute left-0 h-[3px] bottom-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] transition-[width,transform] duration-300 bg-accent-blue"></TabsIndicator>
<TabsTrigger value="peoples" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples</TabsTrigger>
<TabsTrigger value="training" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Entrainement</TabsTrigger>
<TabsTrigger value="abilities" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Compétences</TabsTrigger>
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts</TabsTrigger>
<Tooltip message="Copier le JSON" side="right"><Button icon @click="copy" class="p-2"><Icon icon="radix-icons:clipboard-copy" /></Button></Tooltip>
</TabsList>
<TabsContent value="peoples" class="flex-1 overflow-auto">
</TabsContent>
<TabsContent value="training">
<TrainingViewer :config="config" />
</TabsContent>
<TabsContent value="abilities" class="flex-1 overflow-auto">
</TabsContent>
<TabsContent value="spells" class="flex-1 overflow-auto">
</TabsContent>
</TabsRoot>
<!-- <Collapsible class="border-b border-light-30 dark:border-dark-30 p-1">
<template #label>
<span class="font-bold text-xl">Sorts</span>
</template>
<template #default>
<div class="flex flex-col gap-2 max-h-[50vh] px-4 relative overflow-y-auto">
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex gap-2 items-center">
<span class="text-xl pe-4" :class="{ 'text-light-red dark:text-dark-red': spellsPoints < (data.spells?.length ?? 0) }">Sorts: {{ data.spells?.length ?? 0 }}/{{ spellsPoints }}</span>
<TextInput label="Nom" v-model="spellFilter.text" />
<Combobox label="Rang" v-model="spellFilter.ranks" multiple :options="[['Rang 1', 1], ['Rang 2', 2], ['Rang 3', 3]]" />
<Combobox label="Type" v-model="spellFilter.types" multiple :options="[['Précision', 'precision'], ['Savoir', 'knowledge'], ['Instinct', 'instinct']]" />
<Combobox label="Element" v-model="spellFilter.elements" multiple :options="[['Feu', 'fire'], ['Glace', 'ice'], ['Foudre', 'thunder'], ['Terre', 'earth'], ['Arcane', 'arcana'], ['Air', 'air'], ['Nature', 'nature'], ['Lumière', 'light'], ['Psy', 'psyche']]" />
</div>
<div class="grid gap-4 grid-cols-2">
<div class="py-1 px-2 border border-light-30 dark:border-dark-30 flex flex-col hover:border-light-50 dark:hover:border-dark-50 cursor-pointer" v-for="spell of filterSpells(characterConfig.spells)" :class="{ '!border-accent-blue bg-accent-blue bg-opacity-20': data.spells?.find(e => e === spell.id) }"
@click="() => data.spells?.includes(spell.id) ? data.spells.splice(data.spells.findIndex((e: string) => e === spell.id), 1) : data.spells!.push(spell.id)">
<div class="flex flex-row justify-between">
<span class="text-lg font-bold">{{ spell.name }}</span>
<div class="flex flex-row items-center gap-6">
<div class="flex flex-row text-sm gap-2">
<span v-for="element of spell.elements" :class="elementTexts[element].class" class="border !border-opacity-50 rounded-full !bg-opacity-20 px-2 py-px">{{ elementTexts[element].text }}</span>
</div>
<div class="flex flex-row text-sm gap-1">
<span class="">Rang {{ spell.rank }}</span><span>/</span>
<span class="">{{ spellTypeTexts[spell.type] }}</span><span>/</span>
<span class="">{{ spell.cost }} mana</span><span>/</span>
<span class="">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
</div>
</div>
</div>
<MarkdownRenderer :content="spell.effect" />
</div>
</div>
</div>
</template>
</Collapsible> -->
</template>

View File

@ -1,57 +0,0 @@
<script setup lang="ts">
import { unifySlug } from '~/shared/general.util';
import type { Feature } from '~/types/character';
definePageMeta({
guestsGoesTo: '/user/login',
rights: ['admin', 'editor'],
});
const id = unifySlug(useRouter().currentRoute.value.params.id);
const { add } = useToast();
const homebrew = ref<Feature>({
id: id === 'new' ? -1 : parseInt(id, 10),
list: [],
});
if(id !== 'new')
{
const _homebrew = await useRequestFetch()(`/api/homebrew/${id}`);
if(!_homebrew) throw new Error('Donnée de personnalisation introuvable');
homebrew.value = Object.assign(homebrew.value, _homebrew);
}
function save(force: boolean)
{
}
</script>
<template>
<Head>
<Title>d[any] - Edition de {{ homebrew.name || 'nouvelle personnalisation' }}</Title>
</Head>
<div class="flex flex-col gap-8 align-center">
<div class="flex flex-row gap-4 align-center justify-center">
<div class="flex flex-row gap-4 align-center justify-center">
<Label class="flex items-center justify-between flex-row gap-2">
<span class="pb-1 mx-2 md:p-0">Titre</span>
<input class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
bg-light-20 dark:bg-dark-20 outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
type="text" v-model="homebrew.name">
</Label>
</div>
<div class="self-center">
<Tooltip side="right" message="Ctrl+S"><Button @click="() => save(true)">Enregistrer</Button></Tooltip>
</div>
</div>
<div class="flex flex-1 flex-col min-w-[800px] w-[75vw] max-w-[1200px]">
<div class="m-2 overflow-auto">
</div>
</div>
</div>
</template>

View File

@ -1,3 +0,0 @@
<template>
</template>

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,6 @@ export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const;
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const; export type SpellType = typeof SPELL_TYPES[number];
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const; export type Category = typeof CATEGORIES[number];
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const; export type SpellElement = typeof SPELL_ELEMENTS[number];
export const RESISTANCES = ["stun","bleed","poison","fear","influence","charm","possesion","precision","knowledge","instinct"] as const; export type Resistance = typeof RESISTANCES[number];
export type DoubleIndex<T extends number | string> = [T, number];
@ -109,7 +108,6 @@ export type CharacterConfig = {
peoples: Race[],
training: Record<MainStat, Record<TrainingLevel, TrainingOption[]>>;
abilities: Record<Ability, AbilityConfig>;
resistances: Record<Resistance, ResistanceConfig>;
spells: SpellConfig[];
};
export type SpellConfig = {
@ -128,10 +126,6 @@ export type AbilityConfig = {
name: string;
description: string;
};
export type ResistanceConfig = {
name: string;
statistic: MainStat;
};
export type Race = {
name: string;
description: string;
@ -150,12 +144,19 @@ export type RaceOption = {
spellslots?: number;
};
export type FeatureItem = {
category: "freeaction" | "misc";
category: "misc";
text: string;
} | {
category: "action" | "reaction";
category: "action";
cost: 1 | 2 | 3;
text: string;
} | {
category: "reaction";
cost: 1 | 2;
text: string;
} | {
category: "freeaction";
text: string;
} | {
category: "value";
type: "add" | "remove" | "set";
@ -167,12 +168,7 @@ export type FeatureItem = {
kind: "spells";
asset: string;
}
export type Feature = {
id: number;
name?: string
text?: string;
list?: FeatureItem[];
};
type FeatureCategory = FeatureItem["category"];
export type TrainingOption = {
description: Array<{
text: string;
@ -189,7 +185,8 @@ export type TrainingOption = {
mastery?: keyof CompiledCharacter["mastery"];
spellrank?: SpellType;
defense?: Array<keyof CompiledCharacter["defense"]>;
resistance?: [Resistance, "attack" | "defense"][];
resistance?: Record<MainStat, number>;
bonus?: Record<string, number>;
spell?: string;
//Used during character creation, not used by compiler
@ -240,13 +237,13 @@ export type CompiledCharacter = {
magicinstinct: number;
};
//First is attack, second is defense
resistance: Record<Resistance, [number, number]>;
bonus: Record<string, number>; //Any special bonus goes here
resistance: Record<MainStat, number>;
modifier: Record<MainStat, number>;
abilities: Partial<Record<Ability, number>>;
level: number;
features: Record<Category, string[]>; //Currently: List of training option as text. TODO: Update to a more complex structure later
features: { [K in FeatureCategory]: Array<Extract<FeatureItem, { category: K }>> };
notes: string;
};