New Toaster class, Ability and Resistance removed from config file and choices improvement
This commit is contained in:
parent
17bc232602
commit
c93cc4078c
23
app.vue
23
app.vue
|
|
@ -7,20 +7,19 @@
|
|||
<NuxtPage />
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
<Toaster v-model="list" />
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Content } from './shared/content.util';
|
||||
import { Content } from '#shared/content.util';
|
||||
import * as Floating from '#shared/floating.util';
|
||||
|
||||
provideToaster();
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
onBeforeMount(() => {
|
||||
Content.init();
|
||||
Floating.init();
|
||||
Toaster.init();
|
||||
|
||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||
if(failure) return;
|
||||
|
|
@ -33,11 +32,23 @@ onBeforeMount(() => {
|
|||
unmount();
|
||||
})
|
||||
});
|
||||
|
||||
const { list } = useToast();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ToastRoot[data-type='error'] {
|
||||
@apply border-light-red;
|
||||
@apply dark:border-dark-red;
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply !bg-opacity-50;
|
||||
}
|
||||
.ToastRoot[data-type='success'] {
|
||||
@apply border-light-green;
|
||||
@apply dark:border-dark-green;
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply !bg-opacity-50;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<template>
|
||||
<ProgressRoot class="my-2 relative overflow-hidden bg-light-25 dark:bg-dark-25 w-48 h-3 data-[shape=thin]:h-1 data-[shape=large]:h-6" :data-shape="shape" style="transform: translateZ(0)" >
|
||||
<ProgressIndicator class="bg-light-50 dark:bg-dark-50 h-full w-0 transition-[width] ease-linear" :style="`transition-duration: ${delay}ms; width: ${progress ? 100 : 0}%`" />
|
||||
</ProgressRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { delay = 1500, decreasing = false, shape = 'normal' } = defineProps<{
|
||||
delay?: number
|
||||
decreasing?: boolean
|
||||
shape?: 'thin' | 'normal' | 'large'
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['finish']);
|
||||
|
||||
const progress = ref(false);
|
||||
nextTick(() => {
|
||||
progress.value = true;
|
||||
setTimeout(emit, delay, 'finish');
|
||||
});
|
||||
</script>
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
<template>
|
||||
<ToastProvider>
|
||||
<ToastRoot v-for="toast in model" :key="toast.id" :duration="toast.duration" class="ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 group" :open="toast.state ?? true" @update:open="(state: boolean) => tryClose(toast, state)" :data-type="toast.type ?? 'info'">
|
||||
<div class="grid grid-cols-8 px-3 pt-2 pb-2">
|
||||
<ToastTitle v-if="toast.title" class="font-semibold text-xl col-span-7 text-light-70 dark:text-dark-70" asChild><h4>{{ toast.title }}</h4></ToastTitle>
|
||||
<ToastClose v-if="toast.closeable" aria-label="Close" class="text-xl -translate-y-2 translate-x-4 cursor-pointer"><span aria-hidden>×</span></ToastClose>
|
||||
<ToastDescription v-if="toast.content" class="text-sm col-span-8 text-light-100 dark:text-dark-100" asChild><span>{{ toast.content }}</span></ToastDescription>
|
||||
</div>
|
||||
<TimerProgress v-if="toast.timer" shape="thin" :delay="toast.duration" class="mb-0 mt-0 w-full
|
||||
group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green
|
||||
group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green !bg-opacity-50"
|
||||
@finish="() => tryClose(toast, false)" />
|
||||
</ToastRoot>
|
||||
|
||||
<ToastViewport class="fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72" />
|
||||
</ToastProvider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<ExtraToastConfig[]>();
|
||||
|
||||
function tryClose(config: ExtraToastConfig, state: boolean)
|
||||
{
|
||||
if(!state)
|
||||
{
|
||||
const m = model.value;
|
||||
if(m)
|
||||
{
|
||||
const idx = m?.findIndex(e => e.id === config.id);
|
||||
m[idx].state = false;
|
||||
model.value = m;
|
||||
}
|
||||
setTimeout(() => model.value?.splice(model.value?.findIndex(e => e.id === config.id), 1), 500);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ToastRoot[data-type='error'] {
|
||||
@apply border-light-red;
|
||||
@apply dark:border-dark-red;
|
||||
@apply bg-light-red;
|
||||
@apply dark:bg-dark-red;
|
||||
@apply !bg-opacity-50;
|
||||
}
|
||||
.ToastRoot[data-type='success'] {
|
||||
@apply border-light-green;
|
||||
@apply dark:border-dark-green;
|
||||
@apply bg-light-green;
|
||||
@apply dark:bg-dark-green;
|
||||
@apply !bg-opacity-50;
|
||||
}
|
||||
.ToastRoot[data-state='open'] {
|
||||
animation: slideIn .15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.ToastRoot[data-state='closed'] {
|
||||
animation: hide .1s ease-in;
|
||||
}
|
||||
.ToastRoot[data-swipe='move'] {
|
||||
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||
}
|
||||
.ToastRoot[data-swipe='cancel'] {
|
||||
transform: translateX(0);
|
||||
transition: transform .2s ease-out;
|
||||
}
|
||||
.ToastRoot[data-swipe='end'] {
|
||||
animation: swipeRight .1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes hide {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes swipeRight {
|
||||
from {
|
||||
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
export interface ToastConfig
|
||||
{
|
||||
closeable?: boolean
|
||||
duration: number
|
||||
title?: string
|
||||
content?: string
|
||||
timer?: boolean
|
||||
type?: ToastType
|
||||
}
|
||||
export type ToastType = 'info' | 'success' | 'error';
|
||||
export type ExtraToastConfig = ToastConfig & { id: string, state: boolean };
|
||||
|
||||
let id = 0;
|
||||
|
||||
const [provideToaster, useToast] = createInjectionState(() => {
|
||||
const list = ref<ExtraToastConfig[]>([]);
|
||||
|
||||
function add(config: ToastConfig)
|
||||
{
|
||||
list.value.push({ ...config, id: (++id).toString(), state: true, });
|
||||
}
|
||||
function clear(type?: ToastType)
|
||||
{
|
||||
list.value.forEach(e => { if(e.type !== type) { e.state = false; } });
|
||||
}
|
||||
|
||||
return { list, add, clear }
|
||||
}, { injectionKey: Symbol('toaster') });
|
||||
|
||||
export { provideToaster, useToastWithDefault as useToast };
|
||||
|
||||
function useToastWithDefault()
|
||||
{
|
||||
const toasts = useToast();
|
||||
if(!toasts)
|
||||
{
|
||||
return { list: ref<ExtraToastConfig[]>([]), add: () => {}, clear: () => {} };
|
||||
}
|
||||
return toasts;
|
||||
}
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -31,9 +31,10 @@
|
|||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { format } from '~/shared/general.util';
|
||||
import { iconByType } from '~/shared/content.util';
|
||||
import { format } from '#shared/general.util';
|
||||
import { iconByType } from '#shared/content.util';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
interface File
|
||||
{
|
||||
|
|
@ -69,8 +70,6 @@ definePageMeta({
|
|||
rights: ['admin'],
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
|
||||
const { data: users } = useFetch('/api/admin/users', {
|
||||
transform: (users) => {
|
||||
//@ts-ignore
|
||||
|
|
@ -125,13 +124,13 @@ async function editPermissions(user: User)
|
|||
body: permissionCopy.value,
|
||||
});
|
||||
user.permission = permissionCopy.value;
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'success', content: 'Permissions mises à jour.', timer: true,
|
||||
});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'error', content: (e as any).message, timer: true,
|
||||
});
|
||||
}
|
||||
|
|
@ -146,13 +145,13 @@ async function logout(user: User)
|
|||
|
||||
user.session.length = 0;
|
||||
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'success', content: 'L\'utilisateur vient d\'être déconnecté.', timer: true,
|
||||
});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
toaster.add({
|
||||
Toaster.add({
|
||||
duration: 10000, type: 'error', content: (e as any).message, timer: true,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ const schemaList: Record<string, z.ZodObject<any> | null> = {
|
|||
<script setup lang="ts">
|
||||
import { z } from 'zod/v4';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin'],
|
||||
})
|
||||
const job = ref<string>('');
|
||||
|
||||
const toaster = useToast();
|
||||
const payload = reactive<Record<string, any>>({
|
||||
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
|
||||
to: 'clem31470@gmail.com',
|
||||
|
|
@ -51,7 +51,7 @@ async function fetch()
|
|||
error.value = null;
|
||||
success.value = true;
|
||||
|
||||
toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
|
||||
Toaster.add({ duration: 10000, content: data.value ?? 'Job executé avec succès', type: 'success', timer: true, });
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
|
|
@ -59,7 +59,7 @@ async function fetch()
|
|||
error.value = e as Error;
|
||||
success.value = false;
|
||||
|
||||
toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
|
||||
Toaster.add({ duration: 10000, content: error.value.message, type: 'error', timer: true, });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import PreviewA from '~/components/prose/PreviewA.vue';
|
|||
import { clamp } from '#shared/general.util';
|
||||
import type { SpellConfig } from '~/types/character';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
import { CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '~/shared/character.util';
|
||||
import { abilityTexts, CharacterCompiler, defaultCharacter, elementTexts, spellTypeTexts } from '~/shared/character.util';
|
||||
import { getText } from '~/shared/i18n';
|
||||
import { fakeA } from '~/shared/proses';
|
||||
|
||||
|
|
@ -28,6 +28,11 @@ text-light-green dark:text-dark-green border-light-green dark:border-dark-green
|
|||
text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow
|
||||
text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple
|
||||
*/
|
||||
|
||||
function manageSpell()
|
||||
{
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -88,7 +93,7 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
|
|||
<div class="flex flex-col">
|
||||
<span class="text-lg font-semibold border-b border-light-30 dark:border-dark-30 mb-2">Compétences</span>
|
||||
<div class="grid grid-cols-3 gap-1">
|
||||
<div class="flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70" v-for="(value, ability) of character.abilities"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value }}</span><span>{{ characterConfig.abilities[ability].name }}</span></div>
|
||||
<div class="flex flex-col px-2 items-center text-sm text-light-70 dark:text-dark-70" v-for="(value, ability) of character.abilities"><span class="font-bold text-base text-light-100 dark:text-dark-100">+{{ value }}</span><span>{{ abilityTexts[ability] }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
@ -157,7 +162,7 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
|
|||
</TabsContent>
|
||||
<TabsContent v-if="character.spellslots > 0" value="spells" class="overflow-y-auto max-h-full">
|
||||
<div class="flex flex-1 flex-col ps-8 gap-4 py-2">
|
||||
<div class="flex flex-1 justify-between items-center"><span class="italic text-light-70 dark:text-dark-70 text-sm">{{ character.variables.spells.length }} / {{ character.spellslots }} sorts maitrisés</span><Button icon><Icon icon="radix-icons:plus" class="w-6 h-6"/></Button></div>
|
||||
<div class="flex flex-1 justify-between items-baseline px-2"><div></div><div class="flex gap-4 items-baseline"><span class="italic text-light-70 dark:text-dark-70 text-sm">{{ character.variables.spells.length }} / {{ character.spellslots }} sorts maitrisés</span><Button class="!font-normal" @click="manageSpell">Modifier</Button></div></div>
|
||||
<div class="flex flex-col" v-if="[...(character.lists.spells ?? []), ...character.variables.spells].length > 0">
|
||||
<div class="pb-4 px-2 mt-4 border-b last:border-none border-light-30 dark:border-dark-30 flex flex-col" v-for="spell of [...(character.lists.spells ?? []), ...character.variables.spells].map(e => config.spells.find((f: SpellConfig) => f.id === e)).filter(e => !!e)">
|
||||
<div class="flex flex-row justify-between">
|
||||
|
|
@ -167,8 +172,8 @@ text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-pur
|
|||
<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="" v-if="spell.rank !== 4">Rang {{ spell.rank }}</span><span v-if="spell.rank !== 4">/</span>
|
||||
<span class="" v-if="spell.rank !== 4">{{ spellTypeTexts[spell.type] }}</span><span v-if="spell.rank !== 4">/</span>
|
||||
<span class="">{{ spell.cost }} mana</span><span>/</span>
|
||||
<span class="capitalize">{{ typeof spell.speed === 'string' ? spell.speed : `${spell.speed} minutes` }}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
})
|
||||
const { add } = useToast();
|
||||
|
||||
const { data: characters, error, status } = await useFetch(`/api/character`);
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
|
|
@ -15,7 +14,7 @@ async function deleteCharacter(id: number)
|
|||
status.value = "pending";
|
||||
await useRequestFetch()(`/api/character/${id}`, { method: 'delete' });
|
||||
status.value = "success";
|
||||
add({ content: 'Personnage supprimé', type: 'info', duration: 25000, timer: true, });
|
||||
Toaster.add({ content: 'Personnage supprimé', type: 'info', duration: 25000, timer: true, });
|
||||
characters.value = characters.value?.filter(e => e.id !== id);
|
||||
}
|
||||
async function duplicateCharacter(id: number)
|
||||
|
|
@ -23,7 +22,7 @@ async function duplicateCharacter(id: number)
|
|||
status.value = "pending";
|
||||
const newId = await useRequestFetch()(`/api/character/${id}/duplicate`, { method: 'post' });
|
||||
status.value = "success";
|
||||
add({ content: 'Personnage dupliqué', type: 'info', duration: 25000, timer: true, });
|
||||
Toaster.add({ content: 'Personnage dupliqué', type: 'info', duration: 25000, timer: true, });
|
||||
useRouter().push({ name: 'character-id', params: { id: newId } });
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -39,13 +39,13 @@ import { Content, Editor } from '#shared/content.util';
|
|||
import { button, loading } from '#shared/components.util';
|
||||
import { dom, icon, text } from '#shared/dom.util';
|
||||
import { modal, popper, tooltip } from '#shared/floating.util';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
rights: ['admin', 'editor'],
|
||||
layout: 'null',
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
const { user } = useUserSession();
|
||||
const tree = useTemplateRef('tree'), container = useTemplateRef('container');
|
||||
let editor: Editor;
|
||||
|
|
@ -53,9 +53,9 @@ let editor: Editor;
|
|||
function pull()
|
||||
{
|
||||
Content.pull().then(e => {
|
||||
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
Toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
}).catch(e => {
|
||||
toaster.add({ type: 'success', content: 'Une erreur est survenue durant la récupération des données.', timer: true, duration: 7500 });
|
||||
Toaster.add({ type: 'success', content: 'Une erreur est survenue durant la récupération des données.', timer: true, duration: 7500 });
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
|
@ -64,10 +64,10 @@ function push()
|
|||
const { close } = modal([dom('div', { class: 'flex flex-col gap-4 justify-center items-center' }, [ dom('div', { class: 'text-xl', text: 'Mise à jour des données' }), loading('large') ])], { priority: false, closeWhenOutside: true, });
|
||||
Content.push().then(e => {
|
||||
close();
|
||||
toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
Toaster.add({ type: 'success', content: 'Données mises à jour avec succès.', timer: true, duration: 7500 });
|
||||
}).catch(e => {
|
||||
close();
|
||||
toaster.add({ type: 'success', content: 'Une erreur est survenue durant l\'enregistrement des données.', timer: true, duration: 7500 });
|
||||
Toaster.add({ type: 'success', content: 'Une erreur est survenue durant l\'enregistrement des données.', timer: true, duration: 7500 });
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ definePageMeta({
|
|||
usersGoesTo: '/user/profile',
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
|
||||
const email = ref(''), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
async function submit()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
|
|
@ -33,7 +34,6 @@ definePageMeta({
|
|||
|
||||
const query = useRouter().currentRoute.value.query;
|
||||
|
||||
const toaster = useToast();
|
||||
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ async function submit()
|
|||
{
|
||||
status.value = 'success';
|
||||
|
||||
toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||
Toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||
useRouter().push({ name: 'user-login' });
|
||||
}
|
||||
else
|
||||
|
|
@ -81,7 +81,7 @@ async function submit()
|
|||
status.value = 'error';
|
||||
|
||||
const err = e as any;
|
||||
toaster.add({ content: err?.data?.message ?? err?.message ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||
Toaster.add({ content: err?.data?.message ?? err?.message ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -26,13 +26,13 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
guestsGoesTo: '/user/login',
|
||||
});
|
||||
|
||||
const toaster = useToast();
|
||||
const { user } = useUserSession();
|
||||
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||
|
|
@ -70,19 +70,19 @@ async function submit()
|
|||
{
|
||||
status.value = 'success';
|
||||
|
||||
toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||
Toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||
useRouter().push({ name: 'user-profile' });
|
||||
}
|
||||
else
|
||||
{
|
||||
status.value = 'error';
|
||||
|
||||
toaster.add({ content: result.error ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||
Toaster.add({ content: result.error ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||
}
|
||||
} catch(e) {
|
||||
status.value = 'error';
|
||||
|
||||
toaster.add({ content: (e as Error).message ?? e, duration: 10000, timer: true, type: 'error' });
|
||||
Toaster.add({ content: (e as Error).message ?? e, duration: 10000, timer: true, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -21,14 +21,13 @@
|
|||
import type { ZodError } from 'zod/v4';
|
||||
import { schema, type Login } from '~/schemas/login';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
usersGoesTo: '/user/profile',
|
||||
});
|
||||
|
||||
const { add: addToast, clear: clearToasts } = useToast();
|
||||
|
||||
const state = reactive<Login>({
|
||||
usernameOrEmail: '',
|
||||
password: ''
|
||||
|
|
@ -47,9 +46,9 @@ const toastMessage = ref('');
|
|||
async function submit()
|
||||
{
|
||||
if(state.usernameOrEmail === "")
|
||||
return addToast({ content: 'Veuillez saisir un nom d\'utilisateur ou un email', timer: true, duration: 10000 });
|
||||
return Toaster.add({ content: 'Veuillez saisir un nom d\'utilisateur ou un email', timer: true, duration: 10000 });
|
||||
if(state.password === "")
|
||||
return addToast({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
||||
return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
||||
|
||||
const data = schema.safeParse(state);
|
||||
|
||||
|
|
@ -64,8 +63,8 @@ async function submit()
|
|||
}
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
clearToasts();
|
||||
addToast({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||
await navigateTo('/user/profile');
|
||||
}
|
||||
}
|
||||
|
|
@ -85,12 +84,12 @@ function handleErrors(error: Error | ZodError)
|
|||
{
|
||||
for(const err of (error as ZodError).issues)
|
||||
{
|
||||
return addToast({ content: err.message, timer: true, duration: 10000, type: 'error' });
|
||||
return Toaster.add({ content: err.message, timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return addToast({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
|
||||
return Toaster.add({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
})
|
||||
const { user, clear } = useUserSession();
|
||||
const toaster = useToast();
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
async function revalidateUser()
|
||||
|
|
@ -15,7 +15,7 @@ async function revalidateUser()
|
|||
method: 'post'
|
||||
});
|
||||
loading.value = false;
|
||||
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
||||
Toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
||||
}
|
||||
async function deleteUser()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
import { ZodError } from 'zod/v4';
|
||||
import { schema, type Registration } from '~/schemas/registration';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
|
|
@ -42,7 +43,6 @@ const state = reactive<Registration>({
|
|||
password: ''
|
||||
});
|
||||
|
||||
const { add: addToast, clear: clearToasts } = useToast();
|
||||
const confirmPassword = ref("");
|
||||
|
||||
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
|
||||
|
|
@ -62,13 +62,13 @@ const { data: result, status, error, refresh } = await useFetch('/api/auth/regis
|
|||
async function submit()
|
||||
{
|
||||
if(state.username === '')
|
||||
return addToast({ content: 'Veuillez saisir un nom d\'utilisateur', timer: true, duration: 10000 });
|
||||
return Toaster.add({ content: 'Veuillez saisir un nom d\'utilisateur', timer: true, duration: 10000 });
|
||||
if(state.email === '')
|
||||
return addToast({ content: 'Veuillez saisir une adresse mail', timer: true, duration: 10000 });
|
||||
return Toaster.add({ content: 'Veuillez saisir une adresse mail', timer: true, duration: 10000 });
|
||||
if(state.password === "")
|
||||
return addToast({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
||||
return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
||||
if(state.password !== confirmPassword.value)
|
||||
return addToast({ content: 'Les deux mots de passe saisis ne correspondent pas', timer: true, duration: 10000 });
|
||||
return Toaster.add({ content: 'Les deux mots de passe saisis ne correspondent pas', timer: true, duration: 10000 });
|
||||
|
||||
const data = schema.safeParse(state);
|
||||
|
||||
|
|
@ -83,8 +83,8 @@ async function submit()
|
|||
}
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
clearToasts();
|
||||
addToast({ duration: 10000, content: 'Vous avez été enregistré. Pensez à valider votre adresse mail.', timer: true, type: 'success' });
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous avez été enregistré. Pensez à valider votre adresse mail.', timer: true, type: 'success' });
|
||||
await navigateTo('/user/profile');
|
||||
}
|
||||
}
|
||||
|
|
@ -104,12 +104,12 @@ function handleErrors(error: Error | ZodError)
|
|||
{
|
||||
for(const err of (error as ZodError).issues)
|
||||
{
|
||||
return addToast({ content: err.message, timer: true, duration: 10000, type: 'error' });
|
||||
return Toaster.add({ content: err.message, timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return addToast({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
|
||||
return Toaster.add({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,10 +1,10 @@
|
|||
import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, Feature, FeatureID, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, FeatureItem, Level, MainStat, Resistance, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import { z } from "zod/v4";
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { fakeA } from "#shared/proses";
|
||||
import { button, input, loading, numberpicker, select, toggle } from "#shared/components.util";
|
||||
import { div, dom, icon, mergeClasses, text, type Class } from "#shared/dom.util";
|
||||
import { followermenu, popper, tooltip } from "#shared/floating.util";
|
||||
import { button, input, loading, numberpicker, select, Toaster, toggle } from "#shared/components.util";
|
||||
import { div, dom, icon, text } from "#shared/dom.util";
|
||||
import { followermenu, tooltip } from "#shared/floating.util";
|
||||
import { clamp } from "#shared/general.util";
|
||||
import markdownUtil from "#shared/markdown.util";
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] 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 defaultCharacter: Character = {
|
||||
id: -1,
|
||||
|
|
@ -119,7 +120,7 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|||
freeaction: [],
|
||||
reaction: [],
|
||||
passive: [],
|
||||
spells: character.variables.spells,
|
||||
spells: [],
|
||||
},
|
||||
aspect: "",
|
||||
notes: character.notes ?? "",
|
||||
|
|
@ -169,6 +170,39 @@ export const alignmentTexts: Record<Alignment, string> = {
|
|||
};
|
||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||
|
||||
export const abilityTexts: Record<Ability, string> = {
|
||||
"athletics": "Athlétisme",
|
||||
"acrobatics": "Acrobatique",
|
||||
"intimidation": "Intimidation",
|
||||
"sleightofhand": "Doigté",
|
||||
"stealth": "Discrétion",
|
||||
"survival": "Survie",
|
||||
"investigation": "Enquête",
|
||||
"history": "Histoire",
|
||||
"religion": "Religion",
|
||||
"arcana": "Arcanes",
|
||||
"understanding": "Compréhension",
|
||||
"perception": "Perception",
|
||||
"performance": "Représentation",
|
||||
"medecine": "Médicine",
|
||||
"persuasion": "Persuasion",
|
||||
"animalhandling": "Dressage",
|
||||
"deception": "Mensonge"
|
||||
};
|
||||
|
||||
export const resistanceTexts: Record<Resistance, string> = {
|
||||
'stun': 'Hébètement',
|
||||
'bleed': 'Saignement',
|
||||
'poison': 'Empoisonement',
|
||||
'fear': 'Peur',
|
||||
'influence': 'Influence',
|
||||
'charm': 'Charme',
|
||||
'possesion': 'Possession',
|
||||
'precision': 'Sorts de précision',
|
||||
'knowledge': 'Sorts de savoir',
|
||||
'instinct': 'Sorts d\'instinct',
|
||||
};
|
||||
|
||||
export const CharacterValidation = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
|
|
@ -176,9 +210,9 @@ export const CharacterValidation = z.object({
|
|||
level: z.number().min(1).max(20),
|
||||
aspect: z.number().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number())),
|
||||
leveling: z.record(z.enum(LEVELS.map(String)), z.number()),
|
||||
abilities: z.record(z.enum(ABILITIES), z.number()),
|
||||
training: z.record(z.enum(MAIN_STATS), z.record(z.enum(TRAINING_LEVELS.map(String)), z.number().optional())),
|
||||
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(),
|
||||
|
|
@ -234,12 +268,15 @@ 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));
|
||||
|
||||
return this._result;
|
||||
}
|
||||
get values(): Record<string, number>
|
||||
{
|
||||
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);
|
||||
|
||||
|
|
@ -289,6 +326,7 @@ export class CharacterCompiler
|
|||
|
||||
this._buffer[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;
|
||||
|
||||
return;
|
||||
|
|
@ -322,6 +360,7 @@ export class CharacterCompiler
|
|||
|
||||
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id), 1);
|
||||
|
||||
this._buffer[feature.property]!.min = -Infinity;
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
|
||||
return;
|
||||
|
|
@ -456,6 +495,7 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
];
|
||||
this._stepsHeader = this._steps.map((e, i) =>
|
||||
dom("div", { class: "group flex items-center", }, [
|
||||
i !== 0 ? icon("radix-icons:chevron-right", { class: "w-6 h-6 flex justify-center items-center group-data-[disabled]:text-light-50 dark:group-data-[disabled]:text-dark-50 group-data-[disabled]:hover:border-transparent me-4" }) : undefined,
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue disabled:text-light-50 dark:disabled:text-dark-50 disabled:hover:border-transparent group-data-[state=active]:text-accent-blue cursor-pointer", listeners: { click: () => this.display(i) } }, [text(e.header)]),
|
||||
])
|
||||
);
|
||||
|
|
@ -473,6 +513,16 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
if(step < 0 || step >= this._stepsHeader.length)
|
||||
return;
|
||||
|
||||
for(let i = 0; i < step; i++)
|
||||
{
|
||||
if(!this._steps[i]?.validate(this))
|
||||
{
|
||||
Toaster.add({ title: 'Erreur de validation', content: this._steps[i]!.errorMessage, type: 'error', duration: 25000, timer: true })
|
||||
return;
|
||||
}
|
||||
else
|
||||
{}
|
||||
}
|
||||
if(step !== 0 && this._steps.slice(0, step).some(e => !e.validate(this)))
|
||||
return;
|
||||
|
||||
|
|
@ -492,10 +542,10 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
method: 'post',
|
||||
body: this._character,
|
||||
onResponseError: (e) => {
|
||||
//add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||
Toaster.add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', closeable: true, duration: 25000, timer: true });
|
||||
}
|
||||
});
|
||||
//add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
|
||||
Toaster.add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
|
||||
useRouter().replace({ name: 'character-id-edit', params: { id: this.id } })
|
||||
if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
|
||||
}
|
||||
|
|
@ -506,10 +556,10 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
method: 'post',
|
||||
body: this._character,
|
||||
onResponseError: (e) => {
|
||||
//add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
||||
Toaster.add({ title: 'Erreur d\'enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', closeable: true, duration: 25000, timer: true });
|
||||
}
|
||||
});
|
||||
//add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
|
||||
Toaster.add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
|
||||
if(leave) useRouter().push({ name: 'character-id', params: { id: this.id } });
|
||||
}
|
||||
}
|
||||
|
|
@ -526,9 +576,11 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
if(this._character.leveling.hasOwnProperty(level))
|
||||
{
|
||||
const option = this._character.leveling[level]!;
|
||||
const feature = config.peoples[this._character.people!]!.options[level][option]!;
|
||||
delete this._character.leveling[level];
|
||||
if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];
|
||||
|
||||
this.remove(config.peoples[this._character.people!]!.options[level][option]);
|
||||
this.remove(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -555,9 +607,11 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
|
||||
if(this._character.leveling.hasOwnProperty(level) && this._character.leveling[level] !== choice) //If the given level is already selected, switch to the new choice
|
||||
{
|
||||
this.remove(config.peoples[this._character.people!]!.options[level][this._character.leveling[level]!]);
|
||||
this.add(config.peoples[this._character.people!]!.options[level][choice]);
|
||||
const feature = config.peoples[this._character.people!]!.options[level][this._character.leveling[level]!]!;
|
||||
this.remove(feature);
|
||||
if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];
|
||||
|
||||
this.add(config.peoples[this._character.people!]!.options[level][choice]);
|
||||
this._character.leveling[level] = choice;
|
||||
}
|
||||
else if(!this._character.leveling.hasOwnProperty(level))
|
||||
|
|
@ -586,14 +640,18 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
{
|
||||
if(this._character.training[stat].hasOwnProperty(i))
|
||||
{
|
||||
this.remove(config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]);
|
||||
const feature = config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]!;
|
||||
this.remove(feature);
|
||||
if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];
|
||||
delete this._character.training[stat][i as TrainingLevel];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.remove(config.training[stat][level][this._character.training[stat][level]!]);
|
||||
const feature = config.training[stat][level][this._character.training[stat][level]!]!;
|
||||
this.remove(feature);
|
||||
if(this._character.choices.hasOwnProperty(feature)) delete this._character.choices[feature];
|
||||
this._character.training[stat][level] = choice;
|
||||
this.add(config.training[stat][level][choice]);
|
||||
}
|
||||
|
|
@ -604,95 +662,27 @@ export class CharacterBuilder extends CharacterCompiler
|
|||
this.add(config.training[stat][level][choice]);
|
||||
}
|
||||
}
|
||||
private choose(id: string, choices: number[])
|
||||
handleChoice(element: HTMLElement, feature: string)
|
||||
{
|
||||
const current = this._character.choices[id];
|
||||
const [ feature, effect ] = id.split('-');
|
||||
const option = config.features[feature!]!.effect.find(e => e.id === effect);
|
||||
|
||||
if(option?.category === 'choice')
|
||||
{
|
||||
if(current !== undefined)
|
||||
{
|
||||
current.forEach(e => this.undo(option.options[e]));
|
||||
}
|
||||
if(choices.length > 0)
|
||||
{
|
||||
choices.forEach(e => this.apply(option.options[e]));
|
||||
}
|
||||
|
||||
this._character.choices[id] = choices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PickableFeatureSettings = { state?: boolean, onToggle?: (state: boolean) => void, onChoice?: (options: number[]) => void, disabled?: boolean, class?: { selected?: Class, container?: Class, disabled?: Class }, choices?: Record<string, number[]>, };
|
||||
export class PickableFeature
|
||||
{
|
||||
private _content: HTMLElement;
|
||||
|
||||
private _feature: Feature;
|
||||
|
||||
private _characterChoices?: Record<string, number[]>;
|
||||
private _choiceDom?: HTMLElement;
|
||||
private _choices?: Extract<FeatureItem, { category: 'choice' }>[];
|
||||
|
||||
private _settings?: PickableFeatureSettings;
|
||||
|
||||
constructor(feature: FeatureID, settings?: PickableFeatureSettings)
|
||||
{
|
||||
this._feature = config.features[feature]!;
|
||||
this._settings = settings;
|
||||
|
||||
if(settings?.choices)
|
||||
{
|
||||
this._characterChoices = settings.choices;
|
||||
this._choices = this._feature.effect.filter(e => e.category === 'choice');
|
||||
this._choiceDom = this._choices.length > 0 ? dom('div', { class: 'absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 group-data-[active]:hover:border-light-70 dark:group-data-[active]:hover:border-dark-70 flex p-1 justify-center items-center', listeners: { click: (e) => e.stopImmediatePropagation() ?? this.choose() } }, [ icon('radix-icons:gear') ]) : undefined;
|
||||
}
|
||||
|
||||
this._content = dom("div", { attributes: { 'data-active': settings?.state, 'data-disabled': settings?.disabled ?? false }, class: ["group border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-70 dark:hover:border-dark-70 relative data-[active]:!border-accent-blue data-[active]:bg-accent-blue data-[active]:bg-opacity-20 data-[disabled]:hover:border-light-40 dark:data-[disabled]:hover:border-dark-40 data-[disabled]:opacity-30 data-[disabled]:cursor-default", settings?.class?.container, settings?.class?.selected ? mergeClasses(settings?.class?.selected).split(' ').map(e => `data-[state='active']:${e}`).join(' ') : undefined, settings?.class?.disabled ? mergeClasses(settings?.class?.disabled).split(' ').map(e => `data-[disabled]:${e}`).join(' ') : undefined], listeners: { click: e => this.toggle() }}, [
|
||||
markdownUtil(this._feature.description, undefined, { tags: { a: fakeA } }),
|
||||
this._choiceDom,
|
||||
]);
|
||||
}
|
||||
toggle(state?: boolean)
|
||||
{
|
||||
if(this._content.hasAttribute('data-disabled'))
|
||||
return this._content.hasAttribute('data-active');
|
||||
|
||||
const s = this._content.toggleAttribute('data-active', state);
|
||||
|
||||
this._settings?.onToggle && this._settings?.onToggle(s);
|
||||
|
||||
return s;
|
||||
}
|
||||
choose()
|
||||
{
|
||||
if(!this._choices || this._choices.length === 0)
|
||||
const choices = config.features[feature]!.effect.filter(e => e.category === 'choice');
|
||||
if(choices.length === 0)
|
||||
return;
|
||||
|
||||
const menu = followermenu(this._choiceDom!, [ div('px-24 py-6 flex flex-col items-center text-light-100 dark:text-dark-100', this._choices.map(e => div('flex flex-row items-center', [ text(e.text), div('flex flex-col', Array(e.settings?.amount ?? 1).fill(0).map((_, i) => (
|
||||
select(e.options.map((_e, _i) => ({ text: _e.text, value: _i })), { defaultValue: this._characterChoices![e.id] !== undefined ? this._characterChoices![e.id]![i] : undefined, change: (value) => { this._characterChoices![e.id] ??= []; this._characterChoices![e.id]![i] = value }, class: { container: 'w-32' } })
|
||||
const menu = followermenu(element, [ div('px-24 py-6 flex flex-col items-center text-light-100 dark:text-dark-100', choices.map(e => div('flex flex-row items-center', [ text(e.text), div('flex flex-col', Array(e.settings?.amount ?? 1).fill(0).map((_, i) => (
|
||||
select(e.options.map((_e, _i) => ({ text: _e.text, value: _i })), { defaultValue: this._character.choices![e.id] !== undefined ? this._character.choices![e.id]![i] : undefined, change: (value) => {
|
||||
this._character.choices![e.id] ??= [];
|
||||
this._character.choices![e.id]![i] = value;
|
||||
}, class: { container: 'w-32' } })
|
||||
))) ]))) ], { arrow: true, offset: { mainAxis: 8 }, cover: 'width', placement: 'bottom', priority: false, viewport: document.getElementById('characterEditorContainer') ?? undefined, });
|
||||
}
|
||||
get dom()
|
||||
{
|
||||
return this._content;
|
||||
}
|
||||
}
|
||||
class FeatureTable
|
||||
{
|
||||
constructor(table: Record<number, FeatureID[]>)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
abstract class BuilderTab {
|
||||
protected _builder: CharacterBuilder;
|
||||
protected _content!: Array<Node | string>;
|
||||
static header: string;
|
||||
static description: string;
|
||||
static errorMessage: string;
|
||||
|
||||
constructor(builder: CharacterBuilder) { this._builder = builder; }
|
||||
update() { }
|
||||
|
|
@ -703,6 +693,7 @@ type BuilderTabConstructor = {
|
|||
new (builder: CharacterBuilder): BuilderTab;
|
||||
header: string;
|
||||
description: string;
|
||||
errorMessage: string;
|
||||
validate(builder: CharacterBuilder): boolean;
|
||||
}
|
||||
class PeoplePicker extends BuilderTab
|
||||
|
|
@ -713,6 +704,7 @@ class PeoplePicker extends BuilderTab
|
|||
|
||||
static override header = 'Peuple';
|
||||
static override description = 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.';
|
||||
static override errorMessage = 'Veuillez choisir un peuple pour continuer.';
|
||||
|
||||
constructor(builder: CharacterBuilder)
|
||||
{
|
||||
|
|
@ -732,7 +724,7 @@ class PeoplePicker extends BuilderTab
|
|||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.forEach(f => f?.classList.toggle(e, false)));
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options[i]?.classList.toggle(e, true));
|
||||
}
|
||||
} }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
|
||||
}, attributes: { 'data-people': people.id } }, [div("h-[320px]"), div("text-xl font-bold text-center", [text(people.name)]), div("w-full border-b border-light-50 dark:border-dark-50"), div("text-wrap word-break", [text(people.description)])]),
|
||||
);
|
||||
|
||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
|
||||
|
|
@ -753,6 +745,11 @@ class PeoplePicker extends BuilderTab
|
|||
{
|
||||
this._nameInput.value = this._builder.character.name;
|
||||
this._visibilityInput.setAttribute('data-state', this._builder.character.visibility === "private" ? "checked" : "unchecked");
|
||||
|
||||
if(this._builder.character.people !== undefined)
|
||||
{
|
||||
"border-accent-blue outline-2 outline outline-accent-blue".split(" ").forEach(e => this._options.find(e => e.getAttribute('data-people') === this._builder.character.people)?.classList.toggle(e, true));
|
||||
}
|
||||
}
|
||||
static override validate(builder: CharacterBuilder): boolean
|
||||
{
|
||||
|
|
@ -765,10 +762,12 @@ class LevelPicker extends BuilderTab
|
|||
private _pointsInput: HTMLInputElement;
|
||||
private _healthText: Text;
|
||||
private _manaText: Text;
|
||||
private _options: HTMLDivElement[][];
|
||||
|
||||
private _options: HTMLElement[][];
|
||||
|
||||
static override header = 'Niveaux';
|
||||
static override description = 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.';
|
||||
static override errorMessage = 'Vous avez attribué trop de niveaux.';
|
||||
|
||||
constructor(builder: CharacterBuilder)
|
||||
{
|
||||
|
|
@ -783,7 +782,16 @@ class LevelPicker extends BuilderTab
|
|||
|
||||
this._options = Object.entries(config.peoples[this._builder.character.people!]!.options).map(
|
||||
(level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative left-4" }, [ text(level[0]) ])]),
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => new PickableFeature(option, { disabled: parseInt(level[0], 10) > this._builder.character.level, state: this._builder.character.leveling[parseInt(level[0], 10) as Level] === j, choices: this._builder.character.choices }).dom))
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => {
|
||||
const choice = config.features[option]!.effect.some(e => e.category === 'choice') ? dom('div', { class: 'absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 hover:border-light-70 dark:hover:border-dark-70 flex p-1 justify-center items-center', listeners: { click: (e) => {
|
||||
e.stopImmediatePropagation();
|
||||
this._builder.character.leveling[level[0] as any as Level] === j && this._builder.handleChoice(choice!, config.features[option]!.id);
|
||||
} } }, [ icon('radix-icons:gear') ]) : undefined;
|
||||
return dom("div", { class: ["flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px] relative", { 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': (level[0] as any as Level) <= this._builder.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': this._builder.character.leveling[level[0] as any as Level] === j }], listeners: { click: e => {
|
||||
this._builder.toggleLevelOption(parseInt(level[0]) as Level, j);
|
||||
this.update();
|
||||
}}}, [ dom('span', { class: "text-wrap whitespace-pre", text: config.features[option]!.description }), choice ]);
|
||||
}))
|
||||
]);
|
||||
|
||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
|
||||
|
|
@ -824,14 +832,14 @@ class LevelPicker extends BuilderTab
|
|||
this._builder.updateLevel(this._builder.character.level as Level);
|
||||
|
||||
this._pointsInput.value = (this._builder.character.level - Object.keys(this._builder.character.leveling).length).toString();
|
||||
/* this._options.forEach((e, i) => {
|
||||
this._options.forEach((e, i) => {
|
||||
e[0]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
|
||||
e[1]?.classList.toggle("opacity-30", ((i + 1) as Level) > this._builder.character.level);
|
||||
e[1]?.childNodes.forEach((option, j) => {
|
||||
'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, ((i + 1) as Level) <= this._builder.character.level));
|
||||
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, this._builder.character.leveling[((i + 1) as Level)] === j));
|
||||
});
|
||||
}); */
|
||||
});
|
||||
}
|
||||
static override validate(builder: CharacterBuilder): boolean
|
||||
{
|
||||
|
|
@ -851,6 +859,7 @@ class TrainingPicker extends BuilderTab
|
|||
|
||||
static override header = 'Entrainement';
|
||||
static override description = 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.';
|
||||
static override errorMessage = 'Vous avez dépensé trop de points d\'entrainement.';
|
||||
|
||||
constructor(builder: CharacterBuilder)
|
||||
{
|
||||
|
|
@ -858,8 +867,17 @@ class TrainingPicker extends BuilderTab
|
|||
const statRenderBlock = (stat: MainStat) => {
|
||||
return Object.entries(config.training[stat]).map(
|
||||
(level) => [ div("w-full flex h-px", [div("border-t border-dashed border-light-50 dark:border-dark-50 w-full"), dom('span', { class: "relative" }, [ text(level[0]) ])]),
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => new PickableFeature(option, { state: level[0] == '0' || this._builder.character.training[stat as MainStat][level[0] as any as TrainingLevel] === j, choices: this._builder.character.choices }).dom))
|
||||
])
|
||||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => {
|
||||
const choice = config.features[option]!.effect.some(e => e.category === 'choice') ? dom('div', { class: 'absolute -bottom-px -right-px border border-light-50 dark:border-dark-50 bg-light-10 dark:bg-dark-10 hover:border-light-70 dark:hover:border-dark-70 flex p-1 justify-center items-center', listeners: { click: (e) => {
|
||||
e.stopImmediatePropagation();
|
||||
this._builder.character.training[stat as MainStat][parseInt(level[0], 10) as TrainingLevel] === j && this._builder.handleChoice(choice!, config.features[option]!.id);
|
||||
} } }, [ icon('radix-icons:gear') ]) : undefined;
|
||||
return dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50 relative"], listeners: { click: e => {
|
||||
this._builder.toggleTrainingOption(stat, parseInt(level[0]) as TrainingLevel, j);
|
||||
this.update();
|
||||
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }), choice ]);
|
||||
}))
|
||||
]);
|
||||
}
|
||||
|
||||
this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
|
||||
|
|
@ -912,6 +930,17 @@ class TrainingPicker extends BuilderTab
|
|||
this._pointsInput.value = ((values.training ?? 0) - training).toString();
|
||||
this._healthText.textContent = values.health?.toString() ?? '0';
|
||||
this._manaText.textContent = values.mana?.toString() ?? '0';
|
||||
|
||||
Object.keys(this._options).forEach(stat => {
|
||||
const max = Object.keys(this._builder.character.training[stat as MainStat]).length;
|
||||
this._options[stat as MainStat].forEach((e, i) => {
|
||||
e[0]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
|
||||
e[1]?.classList.toggle("opacity-30", (i as TrainingLevel) > max);
|
||||
e[1]?.childNodes.forEach((option, j) => {
|
||||
'!border-accent-blue bg-accent-blue bg-opacity-20'.split(" ").forEach(_e => (option as HTMLDivElement).classList.toggle(_e, i == 0 || (this._builder.character.training[stat as MainStat][i as TrainingLevel] === j)));
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
static override validate(builder: CharacterBuilder): boolean
|
||||
{
|
||||
|
|
@ -926,70 +955,27 @@ class AbilityPicker extends BuilderTab
|
|||
private _pointsInput: HTMLInputElement;
|
||||
private _options: HTMLDivElement[];
|
||||
|
||||
private _tooltips: Text[] = [];
|
||||
private _maxs: HTMLElement[] = [];
|
||||
|
||||
static override header = 'Compétences';
|
||||
static override description = 'Diversifiez vos possibilités en affectant vos points dans les différentes compétences disponibles.';
|
||||
static override errorMessage = 'Une compétence est incorrectement saisie ou vous avez dépassé le nombre de points à attribuer.';
|
||||
|
||||
constructor(builder: CharacterBuilder)
|
||||
{
|
||||
super(builder);
|
||||
const numberInput = (value?: number, update?: (value: number) => number | undefined) => {
|
||||
const input = dom("input", { class: `w-14 mx-4 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 appearance-none 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`, listeners: {
|
||||
input: (e: Event) => {
|
||||
input.value = (update && update(parseInt(input.value))?.toString()) ?? input.value;
|
||||
},
|
||||
keydown: (e: KeyboardEvent) => {
|
||||
let value = isNaN(parseInt(input.value)) ? '0' : input.value;
|
||||
switch(e.key)
|
||||
{
|
||||
case "ArrowUp":
|
||||
value = clamp(parseInt(value) + 1, 0, 99).toString();
|
||||
break;
|
||||
case "ArrowDown":
|
||||
value = clamp(parseInt(value) - 1, 0, 99).toString();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(input.value !== value)
|
||||
{
|
||||
input.value = (update && update(parseInt(value))?.toString()) ?? value;
|
||||
}
|
||||
}
|
||||
}});
|
||||
|
||||
input.value = value?.toString() ?? "0";
|
||||
return input;
|
||||
};
|
||||
function pushAndReturn<T extends any>(arr: Array<T>, value: T): T
|
||||
{
|
||||
arr.push(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
this._pointsInput = dom("input", { class: `w-14 mx-4 text-light-70 dark:text-dark-70 tabular-nums bg-light-10 dark:bg-dark-10 appearance-none outline-none ps-3 pe-1 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-20 bg-dark-20 border-light-20 dark:border-dark-20`, attributes: { type: "number", disabled: true }});
|
||||
|
||||
this._options = ABILITIES.map((e, i) => div('flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative', [
|
||||
div('flex justify-between', [ numberpicker({ defaultValue: this._builder.character.abilities[e], input: (value) => {
|
||||
const values = this._builder.values;
|
||||
const max = (values[`abilities/${e}/max`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0);
|
||||
|
||||
this._builder.character.abilities[e] = clamp(value, 0, max);
|
||||
Object.assign((this._options[i]?.lastElementChild as HTMLSpanElement | undefined)?.style ?? {}, { width: `${(max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100}%` });
|
||||
this._tooltips[i]!.textContent = `${mainStatTexts[config.abilities[e].max[0]]} (${values[`modifier/${config.abilities[e].max[0]}`] ?? 0}) + ${mainStatTexts[config.abilities[e].max[1]]} (${values[`modifier/${config.abilities[e].max[1]}`] ?? 0}) + ${values[`abilities/${e}/max`] ?? 0}`;
|
||||
this._maxs[i]!.textContent = `/ ${max ?? 0}`;
|
||||
|
||||
const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0);
|
||||
this._pointsInput.value = ((values.ability ?? 0) - abilities).toString();
|
||||
|
||||
return this._builder.character.abilities[e];
|
||||
}}), tooltip(pushAndReturn(this._maxs, dom('span', { class: 'text-lg text-end cursor-pointer', text: '' })), pushAndReturn(this._tooltips, text('')), 'bottom-end')]),
|
||||
dom('span', { class: "text-xl text-center font-bold", text: config.abilities[e].name }),
|
||||
this._options = ABILITIES.map((e, i) => {
|
||||
const max = dom('span', { class: 'text-lg text-end font-bold' });
|
||||
this._maxs.push(max);
|
||||
return div('flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative', [
|
||||
div('flex justify-between', [ numberpicker({ defaultValue: this._builder.character.abilities[e], input: (value) => { this._builder.character.abilities[e] = value; this.update(); }}), max ]),
|
||||
dom('span', { class: "text-xl text-center font-bold", text: abilityTexts[e] }),
|
||||
dom('span', { class: "absolute -bottom-px -left-px h-[3px] bg-accent-blue" }),
|
||||
]));
|
||||
])
|
||||
});
|
||||
|
||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center sticky top-0 bg-light-0 dark:bg-dark-0 w-full z-10", [
|
||||
dom("label", { class: "flex justify-center items-center my-2" }, [
|
||||
|
|
@ -1003,25 +989,33 @@ class AbilityPicker extends BuilderTab
|
|||
}
|
||||
override update()
|
||||
{
|
||||
const values = this._builder.values;
|
||||
const values = this._builder.values, compiled = this._builder.compiled;
|
||||
const abilities = Object.values(this._builder.character.abilities).reduce((p, v) => p + v, 0);
|
||||
|
||||
this._pointsInput.value = ((values.ability ?? 0) - abilities).toString();
|
||||
|
||||
ABILITIES.forEach((e, i) => {
|
||||
const max = (values[`bonus/abilities/${e}`] ?? 0) + (values[`modifier/${config.abilities[e].max[0]}`] ?? 0) + (values[`modifier/${config.abilities[e].max[1]}`] ?? 0);
|
||||
const max = (values[`bonus/abilities/${e}`] ?? 0);
|
||||
|
||||
Object.assign((this._options[i]?.lastElementChild as HTMLSpanElement | undefined)?.style ?? {}, { width: `${(max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100}%` });
|
||||
this._tooltips[i]!.textContent = `${mainStatTexts[config.abilities[e].max[0]]} (${values[`modifier/${config.abilities[e].max[0]}`] ?? 0}) + ${mainStatTexts[config.abilities[e].max[1]]} (${values[`modifier/${config.abilities[e].max[1]}`] ?? 0}) + ${values[`bonus/abilities/${e}`] ?? 0}`;
|
||||
const load = this._options[i]?.lastElementChild as HTMLSpanElement | undefined;
|
||||
const valid = (compiled.abilities[e] ?? 0) <= max;
|
||||
if(load)
|
||||
{
|
||||
Object.assign(load.style ?? {}, { width: `${clamp((max === 0 ? 0 : (this._builder.character.abilities[e] ?? 0) / max) * 100, 0, 100)}%` });
|
||||
'bg-accent-blue'.split(' ').forEach(_e => load.classList.toggle(_e, valid));
|
||||
'bg-light-red dark:bg-dark-red'.split(' ').forEach(_e => load.classList.toggle(_e, !valid));
|
||||
}
|
||||
this._maxs[i]!.textContent = `/ ${max ?? 0}`;
|
||||
})
|
||||
}
|
||||
static override validate(builder: CharacterBuilder): boolean
|
||||
{
|
||||
const values = builder.values;
|
||||
const values = builder.values, compiled = builder.compiled;
|
||||
const abilities = Object.values(builder.character.abilities).reduce((p, v) => p + v, 0);
|
||||
|
||||
return (values.ability ?? 0) - abilities >= 0;
|
||||
console.log(ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)));
|
||||
|
||||
return ABILITIES.map(e => (values[`bonus/abilities/${e}`] ?? 0) >= (compiled.abilities[e] ?? 0)).every(e => e) && (values.ability ?? 0) - abilities >= 0;
|
||||
}
|
||||
}
|
||||
class AspectPicker extends BuilderTab
|
||||
|
|
@ -1036,6 +1030,7 @@ class AspectPicker extends BuilderTab
|
|||
|
||||
static override header = 'Aspect';
|
||||
static override description = 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.';
|
||||
static override errorMessage = 'Veuillez choisir un Aspect.';
|
||||
|
||||
constructor(builder: CharacterBuilder)
|
||||
{
|
||||
|
|
@ -1130,21 +1125,21 @@ class AspectPicker extends BuilderTab
|
|||
}
|
||||
static override validate(builder: CharacterBuilder): boolean
|
||||
{
|
||||
const physic = Object.values(builder.character.training['strength']).length + Object.values(builder.character.training['dexterity']).length + Object.values(builder.character.training['constitution']).length;
|
||||
/* const physic = Object.values(builder.character.training['strength']).length + Object.values(builder.character.training['dexterity']).length + Object.values(builder.character.training['constitution']).length;
|
||||
const mental = Object.values(builder.character.training['intelligence']).length + Object.values(builder.character.training['curiosity']).length;
|
||||
const personality = Object.values(builder.character.training['charisma']).length + Object.values(builder.character.training['psyche']).length;
|
||||
const personality = Object.values(builder.character.training['charisma']).length + Object.values(builder.character.training['psyche']).length; */
|
||||
|
||||
if(builder.character.aspect === undefined)
|
||||
return false;
|
||||
|
||||
const aspect = config.aspects[builder.character.aspect]!
|
||||
/* const aspect = config.aspects[builder.character.aspect]!
|
||||
|
||||
if(physic > aspect.physic.max || physic < aspect.physic.min)
|
||||
return false;
|
||||
if(mental > aspect.mental.max || mental < aspect.mental.min)
|
||||
return false;
|
||||
if(personality > aspect.personality.max || personality < aspect.personality.min)
|
||||
return false;
|
||||
return false; */
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -425,3 +425,84 @@ export function toggle(settings?: { defaultValue?: boolean, change?: (value: boo
|
|||
}, [ div('block w-[18px] h-[18px] translate-x-[2px] will-change-transform transition-transform bg-light-50 dark:bg-dark-50 group-data-[state=checked]:translate-x-[26px] group-data-[disabled]:bg-light-30 dark:group-data-[disabled]:bg-dark-30 group-data-[disabled]:border-light-30 dark:group-data-[disabled]:border-dark-30') ]);
|
||||
return element;
|
||||
}
|
||||
|
||||
export interface ToastConfig
|
||||
{
|
||||
closeable?: boolean
|
||||
duration: number
|
||||
title?: string
|
||||
content?: string
|
||||
timer?: boolean
|
||||
type?: ToastType
|
||||
}
|
||||
type ToastDom = ToastConfig & { dom: HTMLElement };
|
||||
export type ToastType = 'info' | 'success' | 'error';
|
||||
export class Toaster
|
||||
{
|
||||
private static _MAX_DRAG = 130;
|
||||
private static _list: Array<ToastDom> = [];
|
||||
private static _container: HTMLDivElement;
|
||||
|
||||
static init()
|
||||
{
|
||||
Toaster._container = div('fixed bottom-0 right-0 flex flex-col p-6 gap-2 max-w-[512px] z-50 outline-none min-w-72');
|
||||
document.body.appendChild(Toaster._container);
|
||||
}
|
||||
static add(_config: ToastConfig)
|
||||
{
|
||||
let start: number;
|
||||
const dragstart = (e: MouseEvent) => {
|
||||
window.addEventListener('mousemove', dragmove);
|
||||
window.addEventListener('mouseup', dragend);
|
||||
|
||||
start = e.clientX;
|
||||
};
|
||||
const dragmove = (e: MouseEvent) => {
|
||||
const drag = e.clientX - start;
|
||||
if(drag > Toaster._MAX_DRAG)
|
||||
{
|
||||
dragend();
|
||||
config.dom.animate([{ transform: `translateX(${drag}px)` }, { transform: `translateX(150%)` }], { duration: 100, easing: 'ease-out' });
|
||||
Toaster.close(config);
|
||||
}
|
||||
else if(drag > 0)
|
||||
{
|
||||
config.dom.style.transform = `translateX(${drag}px)`;
|
||||
}
|
||||
};
|
||||
const dragend = () => {
|
||||
window.removeEventListener('mousemove', dragmove);
|
||||
window.removeEventListener('mouseup', dragend);
|
||||
|
||||
config.dom.style.transform = `translateX(0px)`;
|
||||
};
|
||||
const config = _config as ToastDom;
|
||||
const loader = config.timer ? div('bg-light-50 dark:bg-dark-50 h-full w-full transition-[width] ease-linear') : undefined;
|
||||
loader?.animate([{ width: '0' }, { width: '100%' }], { duration: config.duration, easing: 'linear' });
|
||||
config.dom = dom('div', { class: 'ToastRoot bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 group select-none', attributes: { 'data-type': config.type, 'data-state': 'open' } }, [
|
||||
div('grid grid-cols-8 px-3 pt-2 pb-2', [
|
||||
config.title ? dom('h4', { class: 'font-semibold text-xl col-span-7 text-light-100 dark:text-dark-100', text: config.title }) : undefined,
|
||||
config.closeable ? dom('span', { class: 'translate-x-4 text-light-100 dark:text-dark-100', listeners: { click: () => Toaster.close(config), } }, [ icon('radix-icons:cross-1', { width: 12, height: 12, noobserver: true, class: 'cursor-pointer' }) ]) : undefined,
|
||||
config.content ? dom('span', { class: 'text-sm col-span-8 text-light-100 dark:text-dark-100', text: config.content }) : undefined,
|
||||
]),
|
||||
config.timer ? dom('div', { class: 'relative overflow-hidden bg-light-25 dark:bg-dark-25 h-1 mb-0 mt-0 w-full group-data-[type=error]:*:bg-light-red dark:group-data-[type=error]:*:bg-dark-red group-data-[type=success]:*:bg-light-green dark:group-data-[type=success]:*:bg-dark-green group-data-[type=error]:bg-light-red dark:group-data-[type=error]:bg-dark-red group-data-[type=success]:bg-light-green dark:group-data-[type=success]:bg-dark-green !bg-opacity-50' }, [ loader ]) : undefined
|
||||
]);
|
||||
config.dom.addEventListener('mousedown', dragstart);
|
||||
config.dom.animate([{ transform: 'translateX(100%)' }, { transform: 'translateX(0)' }], { duration: 150, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' });
|
||||
Toaster._container?.appendChild(config.dom);
|
||||
Toaster._list.push(config);
|
||||
|
||||
setTimeout(() => Toaster.close(config), config.duration);
|
||||
}
|
||||
static clear(type?: ToastType)
|
||||
{
|
||||
Toaster._list.filter(e => e.type !== type || (Toaster.close(e), false));
|
||||
}
|
||||
private static close(config: ToastDom)
|
||||
{
|
||||
config.dom.animate([
|
||||
{ opacity: 1 }, { opacity: 0 },
|
||||
], { easing: 'ease-in', duration: 100 }).onfinish = () => config.dom.remove();
|
||||
Toaster._list = Toaster._list.filter(e => e !== config);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { MarkdownEditor } from "#shared/editor.util";
|
|||
import { fakeA } from "#shared/proses";
|
||||
import { button, combobox, foldable, input, multiselect, numberpicker, select, table, toggle, type Option } from "#shared/components.util";
|
||||
import { confirm, contextmenu, fullblocker, tooltip } from "#shared/floating.util";
|
||||
import { ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
||||
import { ABILITIES, abilityTexts, ALIGNMENTS, alignmentTexts, elementTexts, LEVELS, MAIN_STATS, mainStatShortTexts, mainStatTexts, RESISTANCES, resistanceTexts, SPELL_ELEMENTS, SPELL_TYPES, spellTypeTexts } from "#shared/character.util";
|
||||
import characterConfig from "#shared/character-config.json";
|
||||
import { getID } from "#shared/general.util";
|
||||
import renderMarkdown, { renderText } from "#shared/markdown.util";
|
||||
|
|
@ -32,18 +32,15 @@ export class HomebrewBuilder
|
|||
this._tabsHeader = [
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(0) } }, [text("Peuples")]),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(1) } }, [text("Entrainement")]),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(2) } }, [text("Compétences")]),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(3) } }, [text("Aspect")]),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(4) } }, [text("Sorts")]),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(5) } }, [text("Listes")]),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(2) } }, [text("Aspect")]),
|
||||
dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(3) } }, [text("Sorts")]),
|
||||
//dom("div", { class: "px-2 py-1 border-b border-transparent hover:border-accent-blue cursor-pointer data-[state=active]:border-b-2 data-[state=active]:border-accent-blue", listeners: { click: e => this.display(4) } }, [text("Listes")]),
|
||||
];
|
||||
this._tabsContent = [
|
||||
new PeopleEditor(this, this._config),
|
||||
new TrainingEditor(this, this._config),
|
||||
new AbilityEditor(this, this._config),
|
||||
new AspectEditor(this, this._config),
|
||||
new SpellEditor(this, this._config),
|
||||
/* new ListEditor(this), */
|
||||
];
|
||||
this._content = div('flex-1 outline-none max-w-full w-full overflow-y-auto');
|
||||
this._container.appendChild(div('flex flex-1 flex-col justify-start items-center px-8 w-full h-full overflow-y-hidden', [
|
||||
|
|
@ -237,21 +234,6 @@ class TrainingEditor extends BuilderTab
|
|||
this._statContainer.style.left = `-${tab * 100}%`;
|
||||
}
|
||||
}
|
||||
class AbilityEditor extends BuilderTab
|
||||
{
|
||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
{
|
||||
super(builder, config);
|
||||
|
||||
this._content = [ div('flex px-24 py-4', [table(Object.entries(config.abilities).map(e => ({
|
||||
max1: select(MAIN_STATS.map(e => ({ text: mainStatTexts[e], value: e })), { change: (value) => e[1].max[0] = value, defaultValue: e[1].max[0], class: { container: 'w-full !m-0' } }),
|
||||
max2: select(MAIN_STATS.map(e => ({ text: mainStatTexts[e], value: e })), { change: (value) => e[1].max[1] = value, defaultValue: e[1].max[1], class: { container: 'w-full !m-0' } }),
|
||||
name: input('text', { input: (value) => e[1].name = value, placeholder: 'Nom', defaultValue: e[1].name, class: 'w-full !m-0' }),
|
||||
description: input('text', { input: (value) => e[1].description = value, placeholder: 'Description', defaultValue: e[1].description, class: 'w-full !m-0' }),
|
||||
id: div('w-full !m-0', [ text(e[0]) ]),
|
||||
})), { id: 'ID', name: 'Nom', description: 'Description', max1: 'Stat 1', max2: 'Stat 2' }, { class: { table: 'flex-1' } })] ) ];
|
||||
}
|
||||
}
|
||||
class AspectEditor extends BuilderTab
|
||||
{
|
||||
constructor(builder: HomebrewBuilder, config: CharacterConfig)
|
||||
|
|
@ -396,6 +378,7 @@ export class FeatureEditor
|
|||
dom('span', { class: 'pb-1 md:p-0', text: "Description" }),
|
||||
tooltip(button(icon('radix-icons:clipboard', { width: 20, height: 20 }), () => {
|
||||
MarkdownEditor.singleton.content = this._feature?.effect.map(e => textFromEffect(e)).join('\n') ?? this._feature?.description ?? MarkdownEditor.singleton.content;
|
||||
if(this._feature?.description) this._feature.description = MarkdownEditor.singleton.content;
|
||||
}, 'p-1'), 'Description automatique', 'left'),
|
||||
]),
|
||||
div('p-1 border border-light-40 dark:border-dark-40 w-full bg-light-25 dark:bg-dark-25 min-h-48 max-h-[32rem]', [ MarkdownEditor.singleton.dom ]),
|
||||
|
|
@ -606,8 +589,8 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
|
|||
{ text: 'Arbre de magie (Instinct)', value: { category: 'value', property: 'mastery/magicinstinct', operation: 'add', value: 1 } }
|
||||
] },
|
||||
{ text: 'Compétences', value: [
|
||||
...Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
|
||||
{ text: 'Max de compétence', value: Object.keys(config.abilities).map((e) => ({ text: config.abilities[e as keyof typeof config.abilities].name, value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
|
||||
...ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `abilities/${e}`, operation: 'add', value: 1 } })) as Option<Partial<FeatureItem>>[],
|
||||
{ text: 'Max de compétence', value: ABILITIES.map((e) => ({ text: abilityTexts[e as Ability], value: { category: 'value', property: `bonus/abilities/${e}`, operation: 'add', value: 1 } })) }
|
||||
] },
|
||||
{ text: 'Modifieur', value: [
|
||||
{ text: 'Modifieur de force', value: { category: 'value', property: 'modifier/strength', operation: 'add', value: 1 } },
|
||||
|
|
@ -645,7 +628,7 @@ const featureChoices: Option<Partial<FeatureItem>>[] = [
|
|||
{ text: 'Psyché', egory: 'value', property: 'bonus/defense/psyche', operation: 'add', value: 1 }
|
||||
]} as Partial<FeatureItem>}
|
||||
] },
|
||||
{ text: 'Bonus', value: Object.keys(config.resistances).map((e: Resistance) => ({ text: config.resistances[e]!.name, value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) },
|
||||
{ text: 'Bonus', value: RESISTANCES.map(e => ({ text: resistanceTexts[e as Resistance], value: { category: 'value', property: `resistance/${e}`, operation: 'add', value: 1 } })) },
|
||||
{ text: 'Rang', value: [
|
||||
{ text: 'Sorts de précision', value: { category: 'value', property: 'spellranks/precision', operation: 'add', value: 1 } },
|
||||
{ text: 'Sorts de savoir', value: { category: 'value', property: 'spellranks/knowledge', operation: 'add', value: 1 } },
|
||||
|
|
@ -760,15 +743,15 @@ function textFromEffect(effect: Partial<FeatureItem>): string
|
|||
switch(splited[1])
|
||||
{
|
||||
case 'resistance':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (for.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise for = interdit).' });
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ` aux jets de résistance de ${resistanceTexts[splited[2] as Resistance]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Jets de résistance de ${resistanceTexts[splited[2] as Resistance]} = ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Résistance ${resistanceTexts[splited[2] as Resistance]} = interdit).` });
|
||||
case 'abilities':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${config.abilities[splited[2] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${config.abilities[splited[2] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${config.abilities[splited[2] as Ability].name} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${config.abilities[splited[2] as Ability].name} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${config.abilities[splited[2] as Ability].name} max = interdit).` });
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : effect.operation === 'set' ? textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` }) : textFromValue(effect.value, { prefix: { truely: `Max de ${abilityTexts[splited[2] as Ability]} min à ` }, suffix: { truely: '.' }, falsely: `Opération interdite ( ${abilityTexts[splited[2] as Ability]} max = interdit).` });
|
||||
default: return 'Bonus inconnu';
|
||||
}
|
||||
case 'resistance':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${config.resistances[splited[1] as Resistance]!.name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${config.resistances[splited[1] as Resistance]!.name} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (${config.resistances[splited[1] as Resistance]!.name} = interdit).` });
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${resistanceTexts[splited[1] as Resistance]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `Difficulté des jets de résistance de ${resistanceTexts[splited[1] as Resistance]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (${resistanceTexts[splited[1] as Resistance]} = interdit).` });
|
||||
case 'abilities':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${config.abilities[splited[1] as Ability].name} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${config.abilities[splited[1] as Ability].name}.` });
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: `${abilityTexts[splited[1] as Ability]} `, positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: `${abilityTexts[splited[1] as Ability]} fixé à ` }, suffix: { truely: '.' }, falsely: `Echec automatique de ${abilityTexts[splited[1] as Ability]}.` });
|
||||
case 'modifier':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+' }, suffix: { truely: ` au mod. de ${mainStatTexts[splited[1] as MainStat]}.` } }) : textFromValue(effect.value, { prefix: { truely: `Mod. de ${mainStatTexts[splited[1] as MainStat]} fixé à ` }, suffix: { truely: '.' }, falsely: `Opération interdite (Mod. de ${mainStatShortTexts[splited[1] as MainStat]} = interdit).` });
|
||||
default: break;
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ export function tooltip(container: HTMLElement, txt: string | Text, placement: F
|
|||
delay: delay,
|
||||
content: [ typeof txt === 'string' ? text(txt) : txt ],
|
||||
placement: placement,
|
||||
class: "fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50"
|
||||
class: "fixed hidden TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50 max-w-96"
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS } from "#shared/character.util";
|
||||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS, ALIGNMENTS, RESISTANCES } from "#shared/character.util";
|
||||
|
||||
export type MainStat = typeof MAIN_STATS[number];
|
||||
export type Ability = typeof ABILITIES[number];
|
||||
|
|
@ -8,10 +8,10 @@ export type SpellType = typeof SPELL_TYPES[number];
|
|||
export type Category = typeof CATEGORIES[number];
|
||||
export type SpellElement = typeof SPELL_ELEMENTS[number];
|
||||
export type Alignment = typeof ALIGNMENTS[number];
|
||||
export type Resistance = typeof RESISTANCES[number];
|
||||
|
||||
export type FeatureID = string;
|
||||
export type i18nID = string;
|
||||
export type Resistance = string;
|
||||
|
||||
export type Character = {
|
||||
id: number;
|
||||
|
|
@ -51,9 +51,7 @@ type ItemState = {
|
|||
};
|
||||
export type CharacterConfig = {
|
||||
peoples: Record<string, RaceConfig>;
|
||||
resistances: Record<Resistance, { name: string, statistic: MainStat }>;
|
||||
training: Record<MainStat, Record<TrainingLevel, FeatureID[]>>;
|
||||
abilities: Record<Ability, AbilityConfig>;
|
||||
spells: SpellConfig[];
|
||||
aspects: AspectConfig[];
|
||||
features: Record<FeatureID, Feature>;
|
||||
|
|
@ -108,11 +106,6 @@ export type SpellConfig = {
|
|||
concentration: boolean;
|
||||
tags?: string[];
|
||||
};
|
||||
export type AbilityConfig = {
|
||||
max: [MainStat, MainStat];
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
export type RaceConfig = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue