Feature Builder panel progress
This commit is contained in:
parent
86556ec604
commit
920ce2e1b6
|
|
@ -1,38 +0,0 @@
|
|||
<template>
|
||||
<template v-if="model && model.people !== undefined">
|
||||
<div class="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">
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Points restants</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Button @click="emit('next')">Suivant</Button>
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap justify-center items-center flex-1 gap-12 mx-8 my-4 px-48">
|
||||
<template v-for="ability of config.abilities">
|
||||
<div class="flex flex-col border border-light-50 dark:border-dark-50 p-4 gap-2 w-[200px] relative">
|
||||
<div class="flex justify-between">
|
||||
<NumberFieldRoot :min="0" class="flex w-20 justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
|
||||
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||
</NumberFieldRoot>
|
||||
<Tooltip side="bottom" :message="`${mainStatTexts[ability.max[0]]} (0) + ${mainStatTexts[ability.max[1]]} (0) + 0`"><span class="text-lg text-end cursor-pointer">/ {{ 0 }}</span></Tooltip>
|
||||
</div>
|
||||
<span class="text-xl text-center font-bold">{{ ability.name }}</span>
|
||||
<span class="absolute -bottom-px -left-px h-[3px] bg-accent-blue" :style="{ width: `200px` }"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mainStatTexts, type Character, type CharacterConfig } from '~/types/character';
|
||||
|
||||
const { config } = defineProps<{
|
||||
config: CharacterConfig,
|
||||
}>();
|
||||
const model = defineModel<Character>({ required: true });
|
||||
|
||||
const emit = defineEmits(['next']);
|
||||
</script>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<template>
|
||||
<template v-if="model && model.people !== undefined">
|
||||
<div class="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">
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Physique</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Mental</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Caractère</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Button @click="emit('next')" :disabled="model.aspect === undefined">Enregistrer</Button>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 gap-4 mx-8 my-4">
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Character, CharacterConfig } from '~/types/character';
|
||||
|
||||
const { config } = defineProps<{
|
||||
config: CharacterConfig,
|
||||
}>();
|
||||
const model = defineModel<Character>({ required: true });
|
||||
|
||||
const emit = defineEmits(['next']);
|
||||
</script>
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<template>
|
||||
<template v-if="model && model.character && model.character.people !== undefined">
|
||||
<div class="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">
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Niveau</span>
|
||||
<NumberFieldRoot :min="1" :max="20" v-model="model.character.level" @update:model-value="val => model.updateLevel(val as Level)" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 hover:border-light-50 dark:hover:border-dark-50 has-[:focus]:shadow-raw transition-[box-shadow] has-[:focus]:shadow-light-40 dark:has-[:focus]:shadow-dark-40">
|
||||
<NumberFieldInput class="tabular-nums w-20 bg-transparent px-3 py-1 outline-none caret-light-50 dark:caret-dark-50" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Attributions restantes</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Vie</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Mana</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Button @click="emit('next')">Suivant</Button>
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 gap-4 mx-8 my-4">
|
||||
<template v-for="(level, index) of config.peoples[model.character.people!].options">
|
||||
<div class="w-full flex h-px"><div class="border-t border-dashed border-light-50 dark:border-dark-50 w-full" :class="{ 'opacity-30': index > model.character.level }"></div><span class="sticky top-0">{{ index }}</span></div>
|
||||
<div class="flex flex-row gap-4 justify-center" :class="{ 'opacity-30': index > model.character.level }">
|
||||
<template v-for="(option, i) of level">
|
||||
<div class="flex border border-light-50 dark:border-dark-50 px-4 py-2 w-[400px]" @click="model.toggleLevelOption(parseInt(index as unknown as string, 10) as Level, i)"
|
||||
:class="{ 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': index <= model.character.level, '!border-accent-blue bg-accent-blue bg-opacity-20': model.character.leveling?.some(e => e[0] == index && e[1] === i) ?? false }">
|
||||
<span class="text-wrap whitespace-pre">{{ option.description }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CharacterBuilder } from '#shared/character.util';
|
||||
import type { CharacterConfig, Level } from '~/types/character';
|
||||
|
||||
const { config } = defineProps<{
|
||||
config: CharacterConfig,
|
||||
}>();
|
||||
const model = defineModel<CharacterBuilder>({ required: true });
|
||||
|
||||
const emit = defineEmits(['next']);
|
||||
</script>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<template>
|
||||
<template v-if="model">
|
||||
<div class="flex flex-1 gap-12 px-2 py-4 justify-center items-center">
|
||||
<TextInput label="Nom" v-model="model.character.name" class="flex-none"/>
|
||||
<Switch label="Privé ?" :default-value="model.character.visibility === 'private'" @update:model-value="(e) => model!.character.visibility = e ? 'private' : 'public'" />
|
||||
<Button @click="emit('next')">Suivant</Button>
|
||||
</div>
|
||||
<div class="flex flex-1 gap-4 p-2 overflow-x-auto justify-center">
|
||||
<div v-for="(people, i) of config.peoples" @click="model.character.people = i" class="flex flex-col flex-nowrap gap-2 p-2 border border-light-35 dark:border-dark-35
|
||||
cursor-pointer hover:border-light-70 dark:hover:border-dark-70 w-[320px]" :class="{ '!border-accent-blue outline-2 outline outline-accent-blue': model.character.people === i }">
|
||||
<Avatar :src="people.name" :text="`Image placeholder`" class="h-[320px]" />
|
||||
<span class="text-xl font-bold text-center">{{ people.name }}</span>
|
||||
<span class="w-full border-b border-light-50 dark:border-dark-50"></span>
|
||||
<span class="text-wrap word-break">{{ people.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CharacterBuilder } from '#shared/character.util';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
|
||||
const { config } = defineProps<{
|
||||
config: CharacterConfig,
|
||||
}>();
|
||||
const model = defineModel<CharacterBuilder>();
|
||||
|
||||
const emit = defineEmits(['next']);
|
||||
</script>
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<TrainingViewer :config="config">
|
||||
<template #addin="{ stat }">
|
||||
<div class="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">
|
||||
<Label class="flex items-center justify-between gap-2">
|
||||
<span class="pb-1 mx-2 md:p-0">Points restants</span>
|
||||
<NumberFieldRoot disabled :v-model="0" class="flex justify-center border border-light-25 dark:border-dark-25 bg-light-10 dark:bg-dark-10 text-light-60 dark:text-dark-60">
|
||||
<NumberFieldInput class="tabular-nums w-14 bg-transparent px-3 py-1 outline-none" />
|
||||
</NumberFieldRoot>
|
||||
</Label>
|
||||
<Button @click="emit('next')">Suivant</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ stat, level, option }">
|
||||
<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" @click="toggleOption(stat, parseInt(level as unknown as string, 10) as TrainingLevel, option)" :class="{ /*'opacity-30': level > maxTraining[stat] + 1, 'hover:border-light-60 dark:hover:border-dark-60': level <= maxTraining[stat] + 1, */'!border-accent-blue bg-accent-blue bg-opacity-20': level == 0 || (model.training[stat]?.some(e => e[0] == level && e[1] === option) ?? false) }">
|
||||
<MarkdownRenderer :proses="{ 'a': PreviewA }" :content="config.training[stat][level][option].description.map(e => e.text).join('\n')" />
|
||||
</div>
|
||||
</template>
|
||||
</TrainingViewer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PreviewA from '~/components/prose/PreviewA.vue';
|
||||
import { MAIN_STATS, type Character, type CharacterConfig, type MainStat, type TrainingLevel } from '~/types/character';
|
||||
|
||||
const { config } = defineProps<{
|
||||
config: CharacterConfig,
|
||||
}>();
|
||||
const model = defineModel<Character>({ required: true, });
|
||||
|
||||
const maxTraining = Object.fromEntries(MAIN_STATS.map(e => [e, 0]));
|
||||
|
||||
const emit = defineEmits(['next']);
|
||||
|
||||
function toggleOption(stat: MainStat, level: TrainingLevel, choice: number)
|
||||
{
|
||||
const character = model.value;
|
||||
|
||||
if(level == 0)
|
||||
return;
|
||||
|
||||
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
||||
{
|
||||
if(!character.training[stat].some(e => e[0] == i))
|
||||
return;
|
||||
}
|
||||
|
||||
if(character.training[stat].some(e => e[0] == level))
|
||||
{
|
||||
if(character.training[stat].some(e => e[0] == level && e[1] === choice))
|
||||
{
|
||||
for(let i = 15; i >= level; i --) //Invalidate higher levels
|
||||
{
|
||||
const index = character.training[stat].findIndex(e => e[0] == i);
|
||||
if(index !== -1)
|
||||
character.training[stat].splice(index, 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
character.training[stat].splice(character.training[stat].findIndex(e => e[0] == level), 1, [level, choice]);
|
||||
}
|
||||
else //if(trainingPoints.value && trainingPoints.value > 0)
|
||||
{
|
||||
character.training[stat].push([level, choice]);
|
||||
}
|
||||
|
||||
model.value = character;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -181,7 +181,7 @@ export const _useShortcuts = () => {
|
|||
return false
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
tryOnMounted(() => {
|
||||
metaSymbol.value = macOS.value ? '⌘' : 'Ctrl'
|
||||
})
|
||||
|
||||
|
|
|
|||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -18,15 +18,16 @@
|
|||
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="pl-3 py-1 flex-1 truncate">Personnages</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character' }" class="text-light-70 dark:text-dark-70 border-b-[2px] border-transparent hover:border-accent-blue py-2 hover:!text-opacity-70 flex items-center" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="absolute top-0 left-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30">
|
||||
<NuxtLink :href="{ name: 'character-list' }" class="text-light-70 dark:text-dark-70" active-class="!text-accent-blue"><span class="py-2 px-3 flex-1 truncate">Tous les personnages</span></NuxtLink>
|
||||
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
|
||||
<NuxtLink :href="{ name: 'character-list' }" class="text-light-70 dark:text-dark-70 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
|
||||
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="text-light-70 dark:text-dark-70 hover:bg-light-10 dark:hover:bg-dark-10 hover:text-light-100 dark:hover:text-dark-100 py-2 px-4" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
<div class="absolute top-full left-0 flex w-full justify-center my-4">
|
||||
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] overflow-hidden rounded-[10px] bg-white transition-[width,_height] duration-300 sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||
<div class="absolute top-full left-0 flex w-full justify-center">
|
||||
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
|
||||
</div>
|
||||
</NavigationMenuRoot>
|
||||
<div class="flex items-center px-2 gap-4">
|
||||
|
|
@ -90,26 +91,26 @@ const path = computed(() => route.value.params.path ? decodeURIComponent(unifySl
|
|||
|
||||
await Content.init();
|
||||
const tree = new TreeDOM((item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
|
||||
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private } }, [
|
||||
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 1.5 - 1}em` } }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: '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' }) : undefined,
|
||||
])]);
|
||||
}, (item, depth) => {
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [link({ class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined, [
|
||||
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-inline-start': `${depth / 1.5}em` } }, [link({ class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, active: 'text-accent-blue' }, item.path ? { name: 'explore-path', params: { path: item.path } } : undefined, [
|
||||
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
|
||||
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
|
||||
item.private ? popper(dom('span', { class: 'flex' }, [icon('radix-icons:lock-closed', { class: 'mx-1' })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: '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' }) : undefined,
|
||||
])]);
|
||||
}, (item) => item.navigable);
|
||||
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true));
|
||||
(path.value?.split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
||||
const treeParent = useTemplateRef('treeParent');
|
||||
|
||||
const unmount = useRouter().afterEach((to, from, failure) => {
|
||||
if(failure)
|
||||
return;
|
||||
|
||||
to.name === 'explore-path' && (unifySlug(to.params.path).split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(e, true));
|
||||
to.name === 'explore-path' && (unifySlug(to.params.path ?? '').split('/').map((e, i, a) => a.slice(0, i).join('/')) ?? []).forEach(e => tree.toggle(tree.tree.search('path', e)[0], true));
|
||||
});
|
||||
|
||||
watch(route, () => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ onMounted(() => {
|
|||
|
||||
useShortcuts({
|
||||
"Meta_S": () => builder.save(false),
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
|
|
@ -8,6 +10,7 @@ const { add } = useToast();
|
|||
const { user } = useUserSession();
|
||||
|
||||
const { data: characters, error, status } = await useFetch(`/api/character`);
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
async function deleteCharacter(id: number)
|
||||
{
|
||||
|
|
@ -32,21 +35,48 @@ async function duplicateCharacter(id: number)
|
|||
<Title>d[any] - Mes personnages</Title>
|
||||
</Head>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex align-center justify-center">
|
||||
<NuxtLink v-if="user?.state === 1" :to="{ name: 'character-id-edit', params: { id: 'new' } }"><Button>Nouveau personnage</Button></NuxtLink>
|
||||
<Tooltip v-else side="top" message="Veuillez valider votre email avant de pouvoir créer un personnage."><Button disabled>Nouveau personnage</Button></Tooltip>
|
||||
</div>
|
||||
<div v-if="status === 'pending'" class="flex flex-1 justify-center align-center">
|
||||
<Loading size="large" />
|
||||
</div>
|
||||
<div v-else-if="status === 'success'" class="grid p-6 2xl:grid-cols-3 lg:grid-cols-2 grid-cols-1 gap-4 w-full">
|
||||
<div class="border border-light-30 dark:border-dark-30 p-3 flex flex-row gap-4" v-for="character of characters">
|
||||
<Avatar size="large" icon="radix-icons:person" src="" />
|
||||
<div class="flex flex-1 flex-shrink flex-col truncate">
|
||||
<NuxtLink class="text-xl font-bold hover:text-accent-blue truncate" :to="{ name: 'character-id', params: { id: character.id } }" :title="character.name">{{ character.name }}</NuxtLink>
|
||||
<span class="text-sm truncate">Niveau {{ character.level }}</span>
|
||||
<div class="flex flex-col w-[360px] border border-light-35 dark:border-dark-35" v-for="character of characters">
|
||||
<NuxtLink :to="{ name: 'character-id', params: { id: character.id } }" class="group bg-light-10 dark:bg-dark-10 border-b border-light-35 dark:border-dark-35 p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-row gap-8 ps-4 items-center">
|
||||
<div class="flex flex-1 flex-col gap-2 justify-center">
|
||||
<span class="text-lg font-bold group-hover:text-accent-blue">{{ character.name }}</span>
|
||||
<span class="border-b w-full border-light-50 dark:border-dark-50"></span>
|
||||
<div class="flex flex-row flex-1 items-stretch gap-4">
|
||||
<span class="text-sm">Niveau {{ character.level }}</span>
|
||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||
<span class="text-sm italic">{{ config.peoples[character.people!]?.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-full w-[96px] h-[96px] border border-light-50 dark:border-dark-50 bg-light-100 dark:bg-dark-100 !bg-opacity-10"></div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div class="flex justify-around items-center py-2 px-4 gap-4">
|
||||
<NuxtLink :to="{ name: 'character-id-edit', params: { id: character.id } }" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Editer</NuxtLink>
|
||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||
<NuxtLink @click="duplicateCharacter(character.id)" class="text-sm font-bold cursor-pointer hover:text-accent-blue">Dupliquer</NuxtLink>
|
||||
<span class="w-px h-full bg-light-50 dark:bg-dark-50"></span>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger>
|
||||
<span class="text-sm font-bold text-light-red dark:text-dark-red">Supprimer</span>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent
|
||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Supprimer {{ character.name }} ?</AlertDialogTitle>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Non</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteCharacter(character.id)" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Oui</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</div>
|
||||
<AlertDialogRoot>
|
||||
<!--
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger class="self-start">
|
||||
<Button icon><Icon icon="radix-icons:dots-vertical" /></Button>
|
||||
|
|
@ -84,7 +114,7 @@ async function duplicateCharacter(id: number)
|
|||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
</AlertDialogRoot> -->
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,113 @@
|
|||
<script setup lang="ts">
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
import { FeatureEditor } from '~/shared/feature.util';
|
||||
import { confirm, fullblocker } from '~/shared/floating.util';
|
||||
import { getID, ID_SIZE } from '~/shared/general.util';
|
||||
import type { CharacterConfig, Feature } from '~/types/character';
|
||||
|
||||
//@ts-ignore
|
||||
const config = ref<CharacterConfig>(characterConfig);
|
||||
const featureEditor = new FeatureEditor();
|
||||
|
||||
function copy()
|
||||
{
|
||||
navigator.clipboard.writeText(JSON.stringify(config.value));
|
||||
}
|
||||
|
||||
function createFeature()
|
||||
{
|
||||
const feature: Feature = { id: getID(ID_SIZE), description: '', effect: [] };
|
||||
|
||||
featureEditor.edit(feature).then(feature => {
|
||||
config.value.features[feature.id] = feature;
|
||||
}).catch(() => {}).finally(() => {
|
||||
setTimeout(popup.close, 150);
|
||||
featureEditor.container.setAttribute('data-state', 'inactive');
|
||||
});
|
||||
|
||||
const popup = fullblocker([featureEditor.container], {
|
||||
priority: true, closeWhenOutside: false,
|
||||
});
|
||||
featureEditor.container.setAttribute('data-state', 'active');
|
||||
}
|
||||
function editFeature(id: string)
|
||||
{
|
||||
config.value.features[id] && featureEditor.edit(config.value.features[id]).then(feature => {
|
||||
config.value.features[id] = feature;
|
||||
}).catch(() => {}).finally(() => {
|
||||
setTimeout(popup.close, 150);
|
||||
featureEditor.container.setAttribute('data-state', 'inactive');
|
||||
});
|
||||
|
||||
const popup = fullblocker([featureEditor.container], {
|
||||
priority: true, closeWhenOutside: false,
|
||||
});
|
||||
featureEditor.container.setAttribute('data-state', 'active');
|
||||
}
|
||||
function deleteFeature(id: string)
|
||||
{
|
||||
confirm("Voulez vous vraiment supprimer cet effet ?").then(e => {
|
||||
if(e)
|
||||
{
|
||||
const value = config.value;
|
||||
delete value.features[id];
|
||||
config.value = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Edition de données</Title>
|
||||
</Head>
|
||||
<TabsRoot class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full" default-value="training">
|
||||
<TabsRoot class="flex flex-1 max-w-full flex-col gap-8 justify-start items-center px-8 w-full" default-value="features">
|
||||
<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="peoples" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Peuples ({{ config.peoples.length }})</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>
|
||||
<TabsTrigger value="abilities" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Compétences ({{ Object.keys(config.abilities).length }})</TabsTrigger>
|
||||
<TabsTrigger value="aspects" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Aspects ({{ config.aspects.length }})</TabsTrigger>
|
||||
<TabsTrigger value="spells" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Sorts ({{ config.spells.length }})</TabsTrigger>
|
||||
<TabsTrigger value="features" class="px-2 py-1 border-b border-transparent hover:border-accent-blue">Features ({{ Object.keys(config.features).length }})</TabsTrigger>
|
||||
<Tooltip message="Copier le JSON" side="right"><Button icon @click="copy" class="p-2"><Icon icon="radix-icons:clipboard-copy" /></Button></Tooltip>
|
||||
</TabsList>
|
||||
<div class="flex-1 outline-none max-w-full w-full">
|
||||
<TabsContent value="peoples">
|
||||
</TabsContent>
|
||||
<TabsContent value="training">
|
||||
<TrainingConfigEditor :config="config" />
|
||||
</TabsContent>
|
||||
<TabsContent value="abilities">
|
||||
|
||||
</TabsContent>
|
||||
<TabsContent value="spells">
|
||||
|
||||
</TabsContent>
|
||||
<div class="flex flex-row outline-none max-w-full w-full relative overflow-hidden">
|
||||
<div class="flex flex-1 outline-none max-w-full overflow-hidden">
|
||||
<TabsContent value="peoples" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class=""></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="training" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class=""></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="abilities" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class=""></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="aspects" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class=""></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="spells" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class=""></div>
|
||||
</TabsContent>
|
||||
<TabsContent value="features" class="outline-none flex gap-4 flex-col overflow-hidden">
|
||||
<div class="flex flex-col w-full gap-2 justify-end items-end relative">
|
||||
<Button icon @click="createFeature"><Icon icon="radix-icons:plus" class="w-6 h-6" /></Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 overflow-x-hidden pe-2">
|
||||
<div class="flex flex-row gap-2 w-full border-b border-light-35 dark:border-dark-35 pb-2" v-for="feature of config.features">
|
||||
<div class="w-full flex flex-row px-4 gap-8 items-center">
|
||||
<span class="font-mono">{{ feature.id }}</span>
|
||||
<span class="truncate">{{ feature.description }}</span>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<Button icon @click="editFeature(feature.id)"><Icon icon="radix-icons:pencil-1" /></Button>
|
||||
<Button icon @click="deleteFeature(feature.id)"><Icon icon="radix-icons:trash" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</div>
|
||||
</TabsRoot>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
|||
import type { Ability, Alignment, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import { z } from "zod/v4";
|
||||
import characterConfig from './character-config.json';
|
||||
import { button, fakeA, loading } from "./proses";
|
||||
import { button, fakeA, input, loading } from "./proses";
|
||||
import { div, dom, icon, text } from "./dom.util";
|
||||
import { popper } from "./floating.util";
|
||||
import { clamp } from "./general.util";
|
||||
|
|
@ -83,7 +83,6 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|||
precision: 0,
|
||||
arts: 0,
|
||||
},
|
||||
spells: character.spells ?? [],
|
||||
speed: false,
|
||||
defense: {
|
||||
hardcap: Infinity,
|
||||
|
|
@ -105,8 +104,16 @@ const defaultCompiledCharacter: (character: Character) => CompiledCharacter = (c
|
|||
magicinstinct: 0,
|
||||
},
|
||||
bonus: {},
|
||||
resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record<MainStat, [number, number]>,
|
||||
resistance: {},
|
||||
initiative: 0,
|
||||
capacity: 0,
|
||||
lists: {
|
||||
action: [],
|
||||
freeaction: [],
|
||||
reaction: [],
|
||||
passive: [],
|
||||
spells: character.spells,
|
||||
},
|
||||
aspect: "",
|
||||
notes: character.notes ?? "",
|
||||
});
|
||||
|
|
@ -120,6 +127,15 @@ export const mainStatTexts: Record<MainStat, string> = {
|
|||
"charisma": "Charisme",
|
||||
"psyche": "Psyché",
|
||||
};
|
||||
export const mainStatShortTexts: Record<MainStat, string> = {
|
||||
"strength": "FOR",
|
||||
"dexterity": "DEX",
|
||||
"constitution": "CON",
|
||||
"intelligence": "INT",
|
||||
"curiosity": "CUR",
|
||||
"charisma": "CHA",
|
||||
"psyche": "PSY",
|
||||
};
|
||||
|
||||
export const elementTexts: Record<SpellElement, { class: string, text: string }> = {
|
||||
fire: { class: 'text-light-red dark:text-dark-red border-light-red dark:border-dark-red bg-light-red dark:bg-dark-red', text: 'Feu' },
|
||||
|
|
@ -176,7 +192,7 @@ const stepTexts: Record<number, string> = {
|
|||
4: 'Déterminez l\'Aspect qui vous corresponds et benéficiez de puissants bonus.'
|
||||
};
|
||||
|
||||
type Property = { value: number | string, operation: "set" | "add" };
|
||||
type Property = { value: number | string | false, id: string, operation: "set" | "add" };
|
||||
type PropertySum = { list: Array<Property>, value: number, _dirty: boolean };
|
||||
export class CharacterBuilder
|
||||
{
|
||||
|
|
@ -213,15 +229,10 @@ export class CharacterBuilder
|
|||
|
||||
this._result = defaultCompiledCharacter(this._character);
|
||||
|
||||
Object.entries(character.leveling).forEach(e => {
|
||||
const feature = people.options[parseInt(e[0]) as Level][e[1]]!;
|
||||
feature.effect.map(e => this.apply(e));
|
||||
});
|
||||
Object.entries(character.leveling).forEach(e => this.add(people.options[parseInt(e[0]) as Level][e[1]]!));
|
||||
|
||||
MAIN_STATS.forEach(stat => {
|
||||
Object.entries(character.training[stat]).forEach(option => {
|
||||
config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]!.features?.forEach(this.apply.bind(this));
|
||||
})
|
||||
Object.entries(character.training[stat]).forEach(option => this.add(config.training[stat][parseInt(option[0]) as TrainingLevel][option[1]]))
|
||||
});
|
||||
}
|
||||
load.remove();
|
||||
|
|
@ -372,7 +383,7 @@ export class CharacterBuilder
|
|||
{
|
||||
if(typeof buffer.list[i]!.value === 'string')
|
||||
{
|
||||
if(this._buffer[buffer.list[i]!.value]!._dirty)
|
||||
if(this._buffer[buffer.list[i]!.value as string]!._dirty)
|
||||
{
|
||||
//Put it back in queue since its dependencies haven't been resolved yet
|
||||
queue.push(property);
|
||||
|
|
@ -381,9 +392,9 @@ export class CharacterBuilder
|
|||
else
|
||||
{
|
||||
if(buffer.list[i]?.operation === 'add')
|
||||
sum += this._buffer[buffer.list[i]!.value]!.value;
|
||||
sum += this._buffer[buffer.list[i]!.value as string]!.value;
|
||||
else if(buffer.list[i]?.operation === 'set')
|
||||
sum = this._buffer[buffer.list[i]!.value]!.value;
|
||||
sum = this._buffer[buffer.list[i]!.value as string]!.value;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -478,31 +489,57 @@ export class CharacterBuilder
|
|||
{
|
||||
if(this._character.training[stat].hasOwnProperty(i))
|
||||
{
|
||||
config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]?.features?.forEach(this.undo.bind(this));
|
||||
this.remove(config.training[stat][i as TrainingLevel][this._character.training[stat][i as TrainingLevel]!]);
|
||||
delete this._character.training[stat][i as TrainingLevel];
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
config.training[stat][level][this._character.training[stat][level]!]?.features?.forEach(this.undo.bind(this));
|
||||
this.remove(config.training[stat][level][this._character.training[stat][level]!]);
|
||||
this._character.training[stat][level] = choice;
|
||||
config.training[stat][level][choice]?.features?.forEach(this.apply.bind(this));
|
||||
this.add(config.training[stat][level][choice]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._character.training[stat][level] = choice;
|
||||
config.training[stat][level][choice]?.features?.forEach(this.apply.bind(this));
|
||||
this.add(config.training[stat][level][choice]);
|
||||
}
|
||||
}
|
||||
private add(feature?: Feature)
|
||||
private add(feature?: string)
|
||||
{
|
||||
feature?.effect.forEach(this.apply.bind(this));
|
||||
if(!feature)
|
||||
return;
|
||||
|
||||
config.features[feature]?.effect.forEach(this.apply.bind(this));
|
||||
}
|
||||
private remove(feature?: Feature)
|
||||
private remove(feature?: string)
|
||||
{
|
||||
feature?.effect.forEach(this.undo.bind(this));
|
||||
if(!feature)
|
||||
return;
|
||||
|
||||
config.features[feature]?.effect.forEach(this.undo.bind(this));
|
||||
}
|
||||
private choose(id: string, choices: number[])
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
private apply(feature?: FeatureItem)
|
||||
{
|
||||
|
|
@ -511,30 +548,26 @@ export class CharacterBuilder
|
|||
|
||||
switch(feature.category)
|
||||
{
|
||||
case "feature":
|
||||
this._result.features[feature.kind] ??= [];
|
||||
|
||||
this._result.features[feature.kind]!.push(feature.text);
|
||||
|
||||
return;
|
||||
case "list":
|
||||
if(feature.action === 'add' && !this._result[feature.list].includes(feature.item))
|
||||
this._result[feature.list].push(feature.item);
|
||||
if(feature.action === 'add' && !this._result.lists[feature.list]!.includes(feature.item))
|
||||
this._result.lists[feature.list]!.push(feature.item);
|
||||
else
|
||||
this._result[feature.list] = this._result[feature.list].filter((e: string) => e !== feature.item);
|
||||
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter((e: string) => e !== feature.item);
|
||||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
|
||||
|
||||
this._buffer[feature.property]!.list.push({ operation: feature.operation, value: feature.value });
|
||||
this._buffer[feature.property]!.list.push({ operation: feature.operation, id: feature.id, value: feature.value });
|
||||
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
|
||||
return;
|
||||
case "choice":
|
||||
const choice = this._character.choices[feature.id]!;
|
||||
choice.forEach(e => this.apply(feature.options[e]!));
|
||||
const choice = this._character.choices[feature.id];
|
||||
|
||||
if(choice)
|
||||
choice.forEach(e => this.apply(feature.options[e]!));
|
||||
|
||||
return;
|
||||
default:
|
||||
|
|
@ -548,29 +581,26 @@ export class CharacterBuilder
|
|||
|
||||
switch(feature.category)
|
||||
{
|
||||
case "feature":
|
||||
this._result.features[feature.kind] = this._result.features[feature.kind]!.filter(e => e !== feature.text);
|
||||
|
||||
return;
|
||||
case "list":
|
||||
if(feature.action === 'remove' && !this._result[feature.list].includes(feature.item))
|
||||
this._result[feature.list].push(feature.item);
|
||||
if(feature.action === 'remove' && !this._result.lists[feature.list]!.includes(feature.item))
|
||||
this._result.lists[feature.list]!.push(feature.item);
|
||||
else
|
||||
this._result[feature.list] = this._result[feature.list].filter(e => e !== feature.item);
|
||||
this._result.lists[feature.list] = this._result.lists[feature.list]!.filter(e => e !== feature.item);
|
||||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
|
||||
|
||||
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.operation === feature.operation && e.value === feature.value), 1);
|
||||
this._buffer[feature.property]!.list.splice(this._buffer[feature.property]!.list.findIndex(e => e.id === feature.id), 1);
|
||||
|
||||
this._buffer[feature.property]!._dirty = true;
|
||||
|
||||
return;
|
||||
case "choice":
|
||||
const choice = this._character.choices[feature.id]!;
|
||||
choice.forEach(e => this.undo(feature.options[e]!));
|
||||
delete this._character.choices[feature.id];
|
||||
const choice = this._character.choices[feature.id];
|
||||
|
||||
if(choice)
|
||||
choice.forEach(e => this.undo(feature.options[e]!));
|
||||
|
||||
return;
|
||||
default:
|
||||
|
|
@ -599,14 +629,12 @@ class PeoplePicker implements BuilderTab
|
|||
{
|
||||
this._builder = builder;
|
||||
|
||||
this._nameInput = dom("input", { class: `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) => {
|
||||
this._builder.character.name = this._nameInput.value ?? '';
|
||||
this._nameInput = input('text', {
|
||||
input: (value) => {
|
||||
this._builder.character.name = value ?? '';
|
||||
document.title = `d[any] - Edition de ${this._builder.character.name || 'nouveau personnage'}`;
|
||||
}
|
||||
}});
|
||||
});
|
||||
this._visibilityInput = dom("div", { class: `group mx-3 w-12 h-6 select-none transition-all border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 outline-none
|
||||
data-[state=checked]:bg-light-35 dark:data-[state=checked]:bg-dark-35 hover:border-light-50 dark:hover:border-dark-50 focus:shadow-raw focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 relative py-[2px]`, attributes: { "data-state": "unckecked" }, listeners: {
|
||||
|
|
@ -709,7 +737,7 @@ class LevelPicker implements BuilderTab
|
|||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => 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: option.description }), option.effect.some(e => e.category === 'choice') ? div('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', [ icon('radix-icons:gear') ]) : undefined ])))
|
||||
}}}, [ dom('span', { class: "text-wrap whitespace-pre", text: config.features[option]!.description }), config.features[option]!.effect.some(e => e.category === 'choice') ? div('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', [ icon('radix-icons:gear') ]) : undefined ])))
|
||||
]);
|
||||
|
||||
this._content = [ div("flex flex-1 gap-12 px-2 py-4 justify-center items-center", [
|
||||
|
|
@ -788,7 +816,7 @@ class TrainingPicker implements BuilderTab
|
|||
div("flex flex-row gap-4 justify-center", level[1].map((option, j) => dom("div", { class: ["border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-[400px] hover:border-light-50 dark:hover:border-dark-50"], listeners: { click: e => {
|
||||
this._builder.toggleTrainingOption(stat, parseInt(level[0]) as TrainingLevel, j);
|
||||
this.update();
|
||||
}}}, [ markdownUtil(option.description.map(e => e.text).join('\n'), undefined, { tags: { a: fakeA } }) ])))
|
||||
}}}, [ markdownUtil(config.features[option]!.description, undefined, { tags: { a: fakeA } }) ])))
|
||||
])
|
||||
}
|
||||
this._builder = builder;
|
||||
|
|
|
|||
|
|
@ -704,7 +704,7 @@ export class Editor
|
|||
{
|
||||
e.preventDefault();
|
||||
|
||||
const close = contextmenu(e.clientX, e.clientY, [
|
||||
const { close } = contextmenu(e.clientX, e.clientY, [
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.add("markdown", item); close() }} }, [icon('radix-icons:plus'), text('Ajouter')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.rename(item); close() }} }, [icon('radix-icons:input'), text('Renommer')]),
|
||||
dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { close(); confirm(`Confirmer la suppression de ${item.title}${item.children ? ' et de ses enfants' : ''} ?`).then(e => { if(e) this.remove(item)}) }} }, [icon('radix-icons:trash'), text('Supprimer')]),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ type Listener<K extends keyof HTMLElementEventMap> = | ((ev: HTMLElementEventMap
|
|||
|
||||
export interface NodeProperties
|
||||
{
|
||||
attributes?: Record<string, string | undefined | boolean>;
|
||||
attributes?: Record<string, string | undefined | boolean | number>;
|
||||
text?: string;
|
||||
class?: Class;
|
||||
style?: Record<string, string | undefined | boolean | number> | string;
|
||||
|
|
@ -30,7 +30,7 @@ export function dom<K extends keyof HTMLElementTagNameMap>(tag: K, properties?:
|
|||
|
||||
if(properties?.attributes)
|
||||
for(const [k, v] of Object.entries(properties.attributes))
|
||||
if(typeof v === 'string') element.setAttribute(k, v);
|
||||
if(typeof v === 'string' || typeof v === 'number') element.setAttribute(k, v.toString(10));
|
||||
else if(typeof v === 'boolean') element.toggleAttribute(k, v);
|
||||
|
||||
if(properties?.text)
|
||||
|
|
@ -113,20 +113,24 @@ export interface IconProperties
|
|||
style?: Record<string, string | undefined> | string;
|
||||
class?: Class;
|
||||
}
|
||||
const iconCache: Map<IconProperties & { name: string }, HTMLElement> = new Map();
|
||||
const iconCache: Map<string, HTMLElement> = new Map();
|
||||
export function icon(name: string, properties?: IconProperties): HTMLElement
|
||||
{
|
||||
const key = { ...properties, name };
|
||||
let el;
|
||||
|
||||
if(iconCache.has(key))
|
||||
return iconCache.get(key)!.cloneNode() as HTMLElement;
|
||||
if(iconCache.has(name))
|
||||
el = iconCache.get(name)!.cloneNode() as HTMLElement;
|
||||
else
|
||||
{
|
||||
el = document.createElement('iconify-icon');
|
||||
|
||||
const el = document.createElement('iconify-icon');
|
||||
if(!iconExists(name))
|
||||
loadIcon(name);
|
||||
|
||||
el.setAttribute('icon', name);
|
||||
|
||||
if(!iconExists(name))
|
||||
loadIcon(name);
|
||||
|
||||
el.setAttribute('icon', name);
|
||||
iconCache.set(name, el.cloneNode() as HTMLElement);
|
||||
}
|
||||
|
||||
properties?.mode && el.setAttribute('mode', properties?.mode.toString());
|
||||
properties?.inline && el.toggleAttribute('inline', properties?.inline);
|
||||
|
|
@ -150,7 +154,6 @@ export function icon(name: string, properties?: IconProperties): HTMLElement
|
|||
for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) el.attributeStyleMap.set(k, v);
|
||||
}
|
||||
|
||||
iconCache.set(key, el.cloneNode() as HTMLElement);
|
||||
return el;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,12 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
|||
import { IterMode, Tree } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { dom } from './dom.util';
|
||||
|
||||
const External = Annotation.define<boolean>();
|
||||
const Hidden = Decoration.mark({ class: 'hidden' });
|
||||
const Bullet = Decoration.mark({ class: '*:hidden before:absolute before:top-2 before:left-0 before:inline-block before:w-2 before:h-2 before:rounded before:bg-light-40 dark:before:bg-dark-40 relative ps-4' });
|
||||
const Blockquote = Decoration.line({ class: '*:hidden before:block !ps-4 relative before:absolute before:top-0 before:bottom-0 before:left-0 before:w-1 before:bg-none before:bg-light-30 dark:before:bg-dark-30' });
|
||||
|
||||
const TagTag = tags.special(tags.content);
|
||||
|
||||
const intersects = (a: {
|
||||
from: number;
|
||||
to: number;
|
||||
|
|
@ -36,7 +35,6 @@ const highlight = HighlightStyle.define([
|
|||
{ tag: tags.strong, fontWeight: "bold" },
|
||||
{ tag: tags.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: tags.keyword, color: "#708" },
|
||||
{ tag: TagTag, class: 'cursor-default bg-accent-blue bg-opacity-10 hover:bg-opacity-20 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30' }
|
||||
]);
|
||||
|
||||
class Decorator
|
||||
|
|
@ -111,29 +109,7 @@ export class MarkdownEditor
|
|||
this.view = new EditorView({
|
||||
extensions: [
|
||||
markdown({
|
||||
base: markdownLanguage,
|
||||
extensions: {
|
||||
defineNodes: [
|
||||
{ name: "Tag", style: TagTag },
|
||||
{ name: "TagMark", style: tags.processingInstruction }
|
||||
],
|
||||
parseInline: [{
|
||||
name: "Tag",
|
||||
parse(cx, next, pos) {
|
||||
if (next != 35 || cx.char(pos + 1) == 35) return -1;
|
||||
let elts = [cx.elt("TagMark", pos, pos + 1)];
|
||||
for (let i = pos + 1; i < cx.end; i++) {
|
||||
let next = cx.char(i);
|
||||
if (next == 35)
|
||||
return cx.addElement(cx.elt("Tag", pos, i + 1, elts.concat(cx.elt("TagMark", i, i + 1))));
|
||||
if (next == 92)
|
||||
elts.push(cx.elt("Escape", i, i++ + 2));
|
||||
if (next == 32 || next == 9 || next == 10 || next == 13) break;
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}],
|
||||
}
|
||||
base: markdownLanguage
|
||||
}),
|
||||
history(),
|
||||
search(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,318 @@
|
|||
import type { Ability, CharacterConfig, Feature, FeatureEffect, FeatureItem, MainStat, TrainingOption } from "~/types/character";
|
||||
import { div, dom, icon, text, type NodeChildren } from "./dom.util";
|
||||
import { MarkdownEditor } from "./editor.util";
|
||||
import { button, combobox, fakeA, input, numberpicker, select } from "./proses";
|
||||
import { popper, tooltip } from "./floating.util";
|
||||
import { mainStatShortTexts, mainStatTexts } from "./character.util";
|
||||
import config from "#shared/character-config.json";
|
||||
import { getID, ID_SIZE } from "./general.util";
|
||||
import renderMarkdown from "./markdown.util";
|
||||
|
||||
export class FeatureEditor
|
||||
{
|
||||
private _container: HTMLDivElement;
|
||||
|
||||
private _success?: Function;
|
||||
private _failure?: Function;
|
||||
private _feature?: Feature;
|
||||
|
||||
private _idInput: HTMLInputElement;
|
||||
private _table: HTMLDivElement;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this._idInput = dom("input", { attributes: { 'disabled': true }, class: `mx-4 text-light-70 dark:text-dark-70 appearance-none outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] border bg-light-25 dark:bg-dark-25 border-light-30 dark:border-dark-30` });
|
||||
this._table = div('grid grid-cols-2 gap-4 px-2');
|
||||
this._container = dom('div', { attributes: { 'data-state': 'inactive' }, class: 'border-light-35 dark:border-dark-35 bg-light-10 dark:bg-dark-10 border-l absolute top-0 bottom-0 right-0 w-[10%] data-[state=active]:w-1/2 flex flex-col gap-2 text-light-100 dark:text-dark-100 p-8 transition-[width] transition-delay-[150ms]' }, [
|
||||
div('flex flex-row justify-between items-center', [
|
||||
tooltip(button(icon('radix-icons:check', { width: 20, height: 20 }), () => {
|
||||
this._success!(this._feature);
|
||||
MarkdownEditor.singleton.onChange = undefined;
|
||||
}, 'p-1'), 'Valider', 'left'),
|
||||
dom('label', { class: 'flex justify-center items-center my-2' }, [
|
||||
dom('span', { class: 'pb-1 md:p-0', text: "ID" }),
|
||||
this._idInput
|
||||
]),
|
||||
tooltip(button(icon('radix-icons:cross-1', { width: 20, height: 20 }), () => {
|
||||
this._failure!(this._feature);
|
||||
MarkdownEditor.singleton.onChange = undefined;
|
||||
}, 'p-1'), 'Annuler', 'left'),
|
||||
]),
|
||||
dom('span', { class: 'flex flex-col justify-start items-start my-2 gap-4' }, [
|
||||
div('flex w-full items-center justify-between', [
|
||||
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;
|
||||
}, '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 ]),
|
||||
]),
|
||||
div('flex flex-col gap-2 w-full', [
|
||||
div('flex flex-row justify-between', [
|
||||
dom('h3', { class: 'text-lg font-bold', text: 'Effets' }),
|
||||
tooltip(button(icon('radix-icons:plus', { width: 20, height: 20 }), () => {
|
||||
this._table.appendChild(this._edit({ id: getID(ID_SIZE) }));
|
||||
}, 'p-1'), 'Ajouter', 'left'),
|
||||
]),
|
||||
this._table,
|
||||
])
|
||||
]);
|
||||
}
|
||||
edit(feature: Feature): Promise<Feature>
|
||||
{
|
||||
return new Promise((success, failure) => {
|
||||
this._success = success;
|
||||
this._failure = failure;
|
||||
|
||||
this._feature = JSON.parse(JSON.stringify(feature)) as Feature;
|
||||
|
||||
this._table.replaceChildren(...this._feature.effect.map(this._renderEffect.bind(this)));
|
||||
this._idInput.value = this._feature.id;
|
||||
MarkdownEditor.singleton.onChange = (e) => this._feature!.description = e;
|
||||
MarkdownEditor.singleton.content = this._feature.description;
|
||||
});
|
||||
}
|
||||
private _renderEffect(effect: FeatureItem): HTMLDivElement
|
||||
{
|
||||
const content = div('border border-light-30 dark:border-dark-30 col-span-1', [ div('flex justify-between items-stretch', [
|
||||
div('px-4 flex items-center h-full', [ renderMarkdown(textFromEffect(effect), undefined, { tags: { a: fakeA } }) ]),
|
||||
div('flex', [ tooltip(button(icon('radix-icons:pencil-1'), () => {
|
||||
this._table.replaceChild(this._edit(effect), content);
|
||||
}, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Modifier", "bottom"), tooltip(button(icon('radix-icons:trash'), () => {
|
||||
this._feature!.effect = this._feature!.effect.filter(e => e.id !== effect.id);
|
||||
content.remove();
|
||||
}, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Supprimer", "bottom") ])
|
||||
]) ]);
|
||||
return content;
|
||||
}
|
||||
private _edit(effect: FeatureItem): HTMLDivElement
|
||||
{
|
||||
const match = (effect: FeatureItem): Partial<FeatureItem> | undefined => {
|
||||
switch(effect.category)
|
||||
{
|
||||
case 'value':
|
||||
return choices.find(e => e.value.category === 'value' && e.value.property === effect.property)?.value;
|
||||
/* case 'choice':
|
||||
return choices.find(e => e.value.category === 'choice' && e.value. === effect.property); */
|
||||
case 'list':
|
||||
return choices.find(e => e.value.category === 'list' && e.value.list === effect.list)?.value;
|
||||
}
|
||||
};
|
||||
const approve = () => {
|
||||
const idx = this._feature!.effect.findIndex(e => e.id === buffer.id);
|
||||
|
||||
if(idx === -1)
|
||||
this._feature!.effect.push(buffer);
|
||||
else
|
||||
this._feature!.effect[idx] = buffer;
|
||||
|
||||
this._table.replaceChild(this._renderEffect(buffer), content);
|
||||
}, reject = () => {
|
||||
const idx = this._feature!.effect.findIndex(e => e.id === buffer.id);
|
||||
|
||||
if(idx === -1)
|
||||
content.remove();
|
||||
else
|
||||
this._table.replaceChild(this._renderEffect(effect), content);
|
||||
}
|
||||
let buffer = JSON.parse(JSON.stringify(effect)) as FeatureItem;
|
||||
|
||||
const redraw = () => {
|
||||
let top: NodeChildren = [], bottom: NodeChildren = [];
|
||||
switch(buffer.category)
|
||||
{
|
||||
case 'value':
|
||||
const summaryText = text(textFromEffect(buffer));
|
||||
top = [
|
||||
select([ { text: '+', value: 'add' }, ['speed', 'capacity'].includes(buffer.property) ? { text: '=', value: 'set' } : undefined ], { defaultValue: buffer.operation, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).operation = value as 'add' | 'set'; summaryText.textContent = textFromEffect(buffer); }, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' } }),
|
||||
typeof buffer.value === 'number' ? numberpicker({ defaultValue: buffer.value, input: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); }, class: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' }) : select<`modifier/${MainStat}` | false>([...Object.entries(mainStatShortTexts).map(e => ({ text: 'Mod. de ' + e[1], value: `modifier/${e[0]}` as `modifier/${MainStat}` })), buffer.operation === 'add' ? undefined : { text: 'Interdit', value: false }], { class: { container: 'w-[160px] bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' }, defaultValue: buffer.value, change: (value) => { (buffer as Extract<FeatureEffect, { category: "value" }>).value = value; summaryText.textContent = textFromEffect(buffer); } }),
|
||||
button(icon('radix-icons:update'), () => {
|
||||
(buffer as Extract<FeatureEffect, { category: "value" }>).value = (typeof (buffer as Extract<FeatureEffect, { category: "value" }>).value === 'number' ? '' as any as false : 0);
|
||||
const element = redraw();
|
||||
this._table.replaceChild(element, content);
|
||||
content = element;
|
||||
summaryText.textContent = textFromEffect(buffer);
|
||||
}, 'px-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'),
|
||||
];
|
||||
bottom = [summaryText];
|
||||
break;
|
||||
case 'list':
|
||||
if(buffer.action === 'add')
|
||||
{
|
||||
if(buffer.list === 'spells')
|
||||
{
|
||||
bottom = [ combobox(config.spells.map(e => ({ text: e.name, value: e.id })), { defaultValue: buffer.item, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = value, class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px]' } }) ];
|
||||
}
|
||||
else
|
||||
{
|
||||
const editor = new MarkdownEditor();
|
||||
editor.content = buffer.item;
|
||||
editor.onChange = (item) => (buffer as Extract<FeatureEffect, { category: "list" }>).item = item;
|
||||
|
||||
bottom = [ div('px-4 py-1', [ editor.dom ]) ];
|
||||
}
|
||||
}
|
||||
top = [ select([ { text: 'Ajouter', value: 'add' }, { text: 'Supprimer', value: 'remove' } ], { defaultValue: buffer.action, change: (value) => (buffer as Extract<FeatureEffect, { category: "list" }>).action = value as 'add' | 'remove', class: { container: 'bg-light-25 dark:bg-dark-25 !-m-px h-[36px] w-32' } }) ];
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return div('border border-light-30 dark:border-dark-30 col-span-1 row-span-2', [ div('flex justify-between items-stretch', [
|
||||
div('flex flex-row', [
|
||||
combobox(choices, { defaultValue: match(buffer), class: { container: 'bg-light-25 dark:bg-dark-25 w-[250px] -m-px h-[36px]' }, change: (e) => {
|
||||
buffer = { id: buffer.id, ...e } as FeatureItem;
|
||||
const element = redraw();
|
||||
this._table.replaceChild(element, content);
|
||||
content = element;
|
||||
} }),
|
||||
...top,
|
||||
]),
|
||||
div('flex', [ tooltip(button(icon('radix-icons:check'), approve, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Valider", "bottom"), tooltip(button(icon('radix-icons:cross-1'), reject, 'p-2 -m-px border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50'), "Annuler", "bottom") ])
|
||||
]), div('flex border-t border-light-35 dark:border-dark-35 max-h-[300px] min-h-[36px] overflow-auto', bottom) ]);
|
||||
}
|
||||
|
||||
let content = redraw();
|
||||
return content;
|
||||
}
|
||||
get container()
|
||||
{
|
||||
return this._container;
|
||||
}
|
||||
}
|
||||
|
||||
const choices: Array<{ text: string, value: Partial<FeatureItem> }> = [
|
||||
{ text: 'PV max', value: { category: 'value', property: 'health', operation: 'add', value: 0 }, },
|
||||
{ text: 'Mana max', value: { category: 'value', property: 'mana', operation: 'add', value: 0 }, },
|
||||
{ text: 'Nombre de sorts maitrisés', value: { category: 'value', property: 'spellslots', operation: 'add', value: 0 }, },
|
||||
{ text: 'Nombre d\'œuvres maitrisés', value: { category: 'value', property: 'artslots', operation: 'add', value: 0 }, },
|
||||
{ text: 'Vitesse de course', value: { category: 'value', property: 'speed', operation: 'add', value: 0 }, },
|
||||
{ text: 'Poids max', value: { category: 'value', property: 'capacity', operation: 'add', value: 0 }, },
|
||||
{ text: 'Initiative', value: { category: 'value', property: 'initiative', operation: 'add', value: 0 }, },
|
||||
{ text: 'Points d\'entrainement', value: { category: 'value', property: 'training', operation: 'add', value: 0 }, },
|
||||
{ text: 'Points de compétence', value: { category: 'value', property: 'ability', operation: 'add', value: 0 }, },
|
||||
{ text: 'Sort bonus', value: { category: 'list', list: 'spells', action: 'add' }, },
|
||||
{ text: 'Action', value: { category: 'list', list: 'action', action: 'add' }, },
|
||||
{ text: 'Réaction', value: { category: 'list', list: 'reaction', action: 'add' }, },
|
||||
{ text: 'Action libre', value: { category: 'list', list: 'freeaction', action: 'add' }, },
|
||||
{ text: 'Passif', value: { category: 'list', list: 'passive', action: 'add' }, },
|
||||
{ text: 'Choix', value: { category: 'choice', options: [] }, },
|
||||
];
|
||||
function textFromEffect(effect: FeatureItem)
|
||||
{
|
||||
if(effect.category === 'value')
|
||||
{
|
||||
switch(effect.property)
|
||||
{
|
||||
case 'health':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' PV max.' } }) : textFromValue(effect.value, { prefix: { truely: 'PV max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (PV = interdit).' });
|
||||
case 'mana':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' mana max.' } }) : textFromValue(effect.value, { prefix: { truely: 'Mana max égal à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Mana = interdit).' });
|
||||
case 'spellslots':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' sort(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Sorts maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Sorts = interdit).' });
|
||||
case 'artslots':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' œuvre(s) maitrisé(s).' } }) : textFromValue(effect.value, { prefix: { truely: 'Œuvres maitrisés fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Œuvres = interdit).' });
|
||||
case 'speed':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' case(s) de course.' }, falsely: '+0 cases de course' }) : textFromValue(effect.value, { prefix: { truely: 'Vitesse de course de ' }, suffix: { truely: ' case(s).' }, falsely: 'Déplacement impossible.' });
|
||||
case 'capacity':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' unité(s) d\'quipement.' } }) : textFromValue(effect.value, { prefix: { truely: 'Capacité d\'equipement fixé à ' }, suffix: { truely: ' unité(s).' }, falsely: 'Impossible de posséder du materiel.' });
|
||||
case 'initiative':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' à l\'itiniative.' } }) : textFromValue(effect.value, { prefix: { truely: 'Initiative fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Initiative = interdit).' });
|
||||
case 'training':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) d\'entrainement.' } }) : `Opération interdite (Entrainement fixe).`;
|
||||
case 'ability':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { positive: '+', text: '+Mod. de ' }, suffix: { truely: ' point(s) de compétence.' } }) : `Opération interdite (Compétences fixe).`;
|
||||
default: break;
|
||||
}
|
||||
|
||||
const splited = effect.property.split('/');
|
||||
switch(splited[0])
|
||||
{
|
||||
case 'spellranks':
|
||||
return '';
|
||||
case 'defense':
|
||||
switch(splited[1])
|
||||
{
|
||||
case 'hardcap':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Défense max ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Défense max fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Hardcap = interdit).' });
|
||||
case 'static':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Base de défense ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Base de défense fixé à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Static = interdit).' });
|
||||
case 'activeparry':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active parry = interdit).' });
|
||||
case 'activedodge':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive active ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive active fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Active dodge = interdit).' });
|
||||
case 'passiveparry':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Parade passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Parade passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive parry = interdit).' });
|
||||
case 'passivedodge':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Esquive passive ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Esquive passive fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Passive dodge = interdit).' });
|
||||
default: return 'Défense inconnue.';
|
||||
}
|
||||
case 'mastery':
|
||||
switch(splited[1])
|
||||
{
|
||||
case 'strength':
|
||||
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).' });
|
||||
case 'dexterity':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armes (dex.) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise dex = interdit).' });
|
||||
case 'shield':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des boucliers fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise boucliers = interdit).' });
|
||||
case 'armor':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Maitrise des armure fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise armure = interdit).' });
|
||||
case 'multiattack':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Attaque multiple ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Attaque multiple fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Attaque multiple = interdit).' });
|
||||
case 'magicpower':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Puissance) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise puissance = interdit).' });
|
||||
case 'magicspeed':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Rapidité) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise rapidité = interdit).' });
|
||||
case 'magicelement':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Elements) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise elements = interdit).' });
|
||||
case 'magicinstinct':
|
||||
return effect.operation === 'add' ? textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) ', positive: '+', text: '+Mod. de ' }, suffix: { truely: '.' } }) : textFromValue(effect.value, { prefix: { truely: 'Arbre de magie (Instinct) fixée à ' }, suffix: { truely: '.' }, falsely: 'Opération interdite (Maitrise instinct = interdit).' });
|
||||
default: return 'Maitrise inconnue.';
|
||||
}
|
||||
/* case 'resistance':
|
||||
return splited[1] ? config.resistances[splited[1] as string].name : 'résistance inconnue'; */
|
||||
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}.`}` });
|
||||
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;
|
||||
}
|
||||
|
||||
return `Inconnu ("${effect.property}")`;
|
||||
}
|
||||
else if(effect.category === 'list')
|
||||
{
|
||||
switch(effect.list)
|
||||
{
|
||||
case 'action':
|
||||
case 'reaction':
|
||||
case 'freeaction':
|
||||
case 'passive':
|
||||
return effect.action === 'add' ? effect.item : 'Suppression d\'effet.';
|
||||
case 'spells':
|
||||
return effect.action === 'add' ? `Maitrise du sort "${config.spells.find(e => e.id === effect.item) ?? 'Sort inconnu'}".` : `Perte de maitrise du sort "${config.spells.find(e => e.id === effect.item) ?? 'Sort inconnu'}".`;
|
||||
}
|
||||
}
|
||||
else if(effect.category === 'choice')
|
||||
{
|
||||
return `Choix (WIP)`;
|
||||
}
|
||||
else
|
||||
{
|
||||
return `Inconnu`;
|
||||
}
|
||||
}
|
||||
function textFromValue(value: `modifier/${MainStat}` | number | false, settings?: {
|
||||
prefix?: { text?: string, positive?: string, negative?: string, truely?: string },
|
||||
suffix?: { text?: string, positive?: string, negative?: string, truely?: string },
|
||||
falsely?: string
|
||||
})
|
||||
{
|
||||
if(typeof value === 'string')
|
||||
return `${settings?.prefix?.truely?.replaceAll('(s)', 's') ?? ''}${settings?.prefix?.text?.replaceAll('(s)', 's') ?? ''}${mainStatShortTexts[value.split('/')[1] as MainStat] ?? 'inconnu'}${settings?.suffix?.text?.replaceAll('(s)', 's') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', 's') ?? ''}`;
|
||||
else if(value === false)
|
||||
return settings?.falsely ?? '0';
|
||||
else if(value >= 0)
|
||||
return `${settings?.prefix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.prefix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.positive?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value > 1 ? 's' : '') ?? ''}`;
|
||||
else
|
||||
return `${settings?.prefix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.prefix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${value.toString(10)}${settings?.suffix?.negative?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}${settings?.suffix?.truely?.replaceAll('(s)', value < -1 ? 's' : '') ?? ''}`;
|
||||
}
|
||||
|
|
@ -8,12 +8,13 @@ export interface ContextProperties
|
|||
offset?: number;
|
||||
arrow?: boolean;
|
||||
class?: Class;
|
||||
style?: Record<string, string | undefined | boolean | number> | string;
|
||||
viewport?: HTMLElement;
|
||||
}
|
||||
export interface PopperProperties extends ContextProperties
|
||||
{
|
||||
content?: NodeChildren;
|
||||
delay?: number;
|
||||
viewport?: HTMLElement;
|
||||
|
||||
onShow?: (element: HTMLDivElement) => boolean | void;
|
||||
onHide?: (element: HTMLDivElement) => boolean | void;
|
||||
|
|
@ -36,7 +37,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||
{
|
||||
let shown = false, timeout: Timer;
|
||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]);
|
||||
const content = dom('div', { class: ['fixed hidden', properties?.class], attributes: { 'data-state': 'closed' } }, [...(properties?.content ?? []), arrow]);
|
||||
const content = dom('div', { class: ['fixed hidden', properties?.class], style: properties?.style, attributes: { 'data-state': 'closed' } }, [...(properties?.content ?? []), arrow]);
|
||||
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
||||
|
||||
function update()
|
||||
|
|
@ -151,7 +152,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties): H
|
|||
|
||||
return container;
|
||||
}
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: ContextProperties): () => void
|
||||
export function contextmenu(x: number, y: number, content: NodeChildren, properties?: ContextProperties & { blur?: () => void })
|
||||
{
|
||||
const virtual = {
|
||||
getBoundingClientRect() {
|
||||
|
|
@ -167,9 +168,10 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
|||
};
|
||||
},
|
||||
};
|
||||
const rect = properties?.viewport?.getBoundingClientRect() ?? 'viewport';
|
||||
|
||||
const arrow = svg('svg', { class: 'absolute fill-light-35 dark:fill-dark-35', attributes: { width: "10", height: "7", viewBox: "0 0 30 10" } }, [svg('polygon', { attributes: { points: "0,0 30,0 15,10" } })]);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class] }, content);
|
||||
const container = dom('div', { class: ['fixed bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 z-50', properties?.class], style: properties?.style }, content);
|
||||
|
||||
function update()
|
||||
{
|
||||
|
|
@ -178,9 +180,10 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
|||
strategy: 'fixed',
|
||||
middleware: [
|
||||
properties?.offset ? FloatingUI.offset(properties?.offset) : undefined,
|
||||
FloatingUI.flip(),
|
||||
properties?.offset ? FloatingUI.shift({ padding: properties?.offset }) : undefined,
|
||||
FloatingUI.shift({ rootBoundary: rect }),
|
||||
properties?.offset ? FloatingUI.shift({ padding: properties?.offset, rootBoundary: rect }) : undefined,
|
||||
properties?.offset && properties?.arrow ? FloatingUI.arrow({ element: arrow, padding: 8 }) : undefined,
|
||||
FloatingUI.hide({ rootBoundary: rect }),
|
||||
]
|
||||
}).then(({ x, y, placement, middlewareData }) => {
|
||||
Object.assign(container.style, {
|
||||
|
|
@ -242,16 +245,27 @@ export function contextmenu(x: number, y: number, content: NodeChildren, propert
|
|||
|
||||
container.remove();
|
||||
stop();
|
||||
properties?.blur && properties.blur();
|
||||
}
|
||||
|
||||
return close;
|
||||
return { close, container, content };
|
||||
}
|
||||
export function tooltip(container: HTMLElement, txt: string, placement: FloatingUI.Placement, delay?: number): HTMLElement
|
||||
{
|
||||
return popper(container, {
|
||||
arrow: true,
|
||||
offset: 8,
|
||||
delay: delay,
|
||||
content: [ text(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"
|
||||
});
|
||||
}
|
||||
|
||||
export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||
export function fullblocker(content: NodeChildren, properties?: ModalProperties)
|
||||
{
|
||||
const _modalBlocker = dom('div', { class: [' absolute top-0 left-0 bottom-0 right-0 z-0', { 'bg-light-0 dark:bg-dark-0 opacity-70': properties?.priority ?? false }], listeners: { click: properties?.closeWhenOutside ? (() => _modal.remove()) : undefined } });
|
||||
const _closer = properties?.priority ? undefined : dom('span', { class: 'absolute top-4 right-4', text: '×', listeners: { click: () => _modal.remove() } });
|
||||
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content)])
|
||||
const _modal = dom('div', { class: 'fixed flex justify-center items-center top-0 left-0 bottom-0 right-0 inset-0 z-40' }, [ _modalBlocker, ...content]);
|
||||
|
||||
teleport.appendChild(_modal);
|
||||
|
||||
|
|
@ -259,6 +273,10 @@ export function modal(content: NodeChildren, properties?: ModalProperties)
|
|||
close: () => _modal.remove(),
|
||||
}
|
||||
}
|
||||
export function modal(content: NodeChildren, properties?: ModalProperties)
|
||||
{
|
||||
return fullblocker([ dom('div', { class: 'max-h-[85vh] max-w-[450px] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 text-light-100 dark:text-dark-100 z-10 relative' }, content) ], properties);
|
||||
}
|
||||
|
||||
export function confirm(title: string): Promise<boolean>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export function unifySlug(slug: string | string[]): string
|
|||
export function getID(length: number)
|
||||
{
|
||||
for (var id = [], i = 0; i < length; i++)
|
||||
id.push((16 * Math.random() | 0).toString(16));
|
||||
id.push((36 * Math.random() | 0).toString(36));
|
||||
return id.join("");
|
||||
}
|
||||
export function group<
|
||||
|
|
|
|||
196
shared/proses.ts
196
shared/proses.ts
|
|
@ -1,11 +1,11 @@
|
|||
import { dom, icon, type NodeChildren, type Node, type NodeProperties, type Class, mergeClasses } from "#shared/dom.util";
|
||||
import { dom, icon, type NodeChildren, type Node, type NodeProperties, type Class, mergeClasses, text, div } from "#shared/dom.util";
|
||||
import { parseURL } from 'ufo';
|
||||
import render from "#shared/markdown.util";
|
||||
import { popper } from "#shared/floating.util";
|
||||
import { contextmenu, popper } from "#shared/floating.util";
|
||||
import { Canvas } from "#shared/canvas.util";
|
||||
import { Content, iconByType, type LocalContent } from "#shared/content.util";
|
||||
import type { RouteLocationAsRelativeTyped, RouteMapGeneric } from "vue-router";
|
||||
import { unifySlug } from "#shared/general.util";
|
||||
import { clamp, unifySlug } from "#shared/general.util";
|
||||
|
||||
export type CustomProse = (properties: any, children: NodeChildren) => Node;
|
||||
export type Prose = { class: string } | { custom: CustomProse };
|
||||
|
|
@ -230,4 +230,194 @@ export function button(content: Node, onClick?: () => void, cls?: Class)
|
|||
return dom('button', { class: [`text-light-100 dark:text-dark-100 font-semibold hover:bg-light-30 dark:hover:bg-dark-30 inline-flex items-center justify-center bg-light-25 dark:bg-dark-25 leading-none outline-none
|
||||
border border-light-25 dark:border-dark-25 hover:border-light-30 dark:hover:border-dark-30 active:border-light-40 dark:active:border-dark-40 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
||||
disabled:bg-light-10 dark:disabled:bg-dark-10 disabled:border-none disabled:text-light-50 dark:disabled:text-dark-50`, cls], listeners: { click: onClick } }, [ content ]);
|
||||
}
|
||||
export function select<T extends NonNullable<any>>(options: Array<string | undefined> | Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||
{
|
||||
const textFromValue = (value?: T): string => {
|
||||
if(!value)
|
||||
return '';
|
||||
const found = options.find(e => (e as { value: string } | undefined)?.value === value || e === value);
|
||||
if(!found)
|
||||
return '';
|
||||
return (found as { text: string } | undefined)?.text ?? found as string;
|
||||
};
|
||||
let close: Function | undefined;
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const textValue = text(textFromValue(settings?.defaultValue));
|
||||
const optionElements = options.map(e => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
return dom('div', { listeners: { click: () => {
|
||||
let text, value;
|
||||
if(typeof e === 'string')
|
||||
{
|
||||
text = value = e;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = e.text;
|
||||
value = e.value;
|
||||
}
|
||||
|
||||
textValue.textContent = text;
|
||||
settings?.change && settings?.change(value);
|
||||
close && close();
|
||||
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text((e as { text: string } | undefined)?.text ?? e as string) ]);
|
||||
});
|
||||
const select = dom('div', { listeners: { click: () => {
|
||||
if(disabled)
|
||||
return;
|
||||
|
||||
const box = select.getBoundingClientRect();
|
||||
close = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` } }).close;
|
||||
} }, class: ['mx-4 inline-flex items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1 bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark: data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ dom('span', {}, [ textValue ]), icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(select, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
select.toggleAttribute('data-disabled', disabled);
|
||||
},
|
||||
})
|
||||
return select;
|
||||
}
|
||||
export function combobox<T extends NonNullable<any>>(options: Array<string | undefined> | Array<{ text: string, value: T } | undefined>, settings?: { defaultValue?: T, change?: (value: T) => void, class?: { container?: Class, popup?: Class, option?: Class }, disabled?: boolean }): HTMLElement
|
||||
{
|
||||
const textFromValue = (value?: T): string => {
|
||||
if(!value)
|
||||
return '';
|
||||
const found = options.find(e => (e as { value: string } | undefined)?.value === value || e === value);
|
||||
if(!found)
|
||||
return '';
|
||||
return (found as { text: string } | undefined)?.text ?? found as string;
|
||||
};
|
||||
let context: { container: HTMLElement, content: NodeChildren, close: () => void };
|
||||
let selected = true;
|
||||
|
||||
const show = () => {
|
||||
if(disabled || (context && context.container.parentElement))
|
||||
return;
|
||||
|
||||
const box = container.getBoundingClientRect();
|
||||
context = contextmenu(box.x, box.y + box.height, optionElements.filter(e => !!e).length > 0 ? optionElements.map(e => e?.dom) : [ div('text-light-60 dark:text-dark-60 italic text-center px-2 py-1', [ text('Aucune option') ]) ], { placement: "bottom-start", class: ['flex flex-col max-h-[320px] overflow-auto', settings?.class?.popup], style: { "min-width": `${box.width}px` }, blur: () => { if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red') } });
|
||||
if(!selected) container.classList.remove('!border-light-red', 'dark:!border-dark-red');
|
||||
};
|
||||
const hide = () => {
|
||||
if(!context || !context.container.parentElement)
|
||||
return;
|
||||
|
||||
context.close();
|
||||
if(!selected) container.classList.add('!border-light-red', 'dark:!border-dark-red');
|
||||
};
|
||||
|
||||
let disabled = settings?.disabled ?? false;
|
||||
const optionElements = options.map((e, i) => {
|
||||
if(e === undefined)
|
||||
return;
|
||||
|
||||
return { item: e, dom: dom('div', { listeners: { click: () => {
|
||||
let text, value;
|
||||
if(typeof e === 'string')
|
||||
{
|
||||
text = value = e;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = e.text;
|
||||
value = e.value;
|
||||
}
|
||||
|
||||
select.value = text;
|
||||
settings?.change && settings?.change(value);
|
||||
selected = true;
|
||||
hide();
|
||||
} }, class: ['hover:bg-light-30 dark:hover:bg-dark-30 text-light-70 dark:text-dark-70 hover:text-light-100 dark:hover:text-dark-100 py-1 px-2 cursor-pointer', settings?.class?.option] }, [ text((e as { text: string } | undefined)?.text ?? e as string) ]) };
|
||||
});
|
||||
const select = dom('input', { listeners: { focus: show, input: () => {
|
||||
context && context?.container.replaceChildren(...optionElements.filter(e => {
|
||||
if(e === undefined)
|
||||
return false;
|
||||
if(typeof e.item === 'string')
|
||||
return (e.item as string).toLowerCase().includes(select.value.toLowerCase());
|
||||
return e.item.text.toLowerCase().includes(select.value.toLowerCase());
|
||||
}).map(e => e!.dom));
|
||||
selected = false;
|
||||
if(!context || !context.container.parentElement) container.classList.add('!border-light-red', 'dark:!border-dark-red')
|
||||
} }, attributes: { type: 'text', }, class: 'flex-1 outline-none px-3 leading-none appearance-none py-1 bg-light-25 dark:bg-dark-25 disabled:bg-light-20 dark:disabled:bg-dark-20' });
|
||||
select.value = textFromValue(settings?.defaultValue);
|
||||
|
||||
const container = dom('label', { class: ['inline-flex outline-none px-3 items-center justify-between text-sm font-semibold leading-none gap-1 bg-light-25 dark:bg-dark-25 border border-light-35 dark:border-dark-35 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:border-light-25 dark:data-[disabled]:border-dark-25 data-[disabled]:bg-light-20 dark: data-[disabled]:bg-dark-20', settings?.class?.container] }, [ select, icon('radix-icons:caret-down') ]);
|
||||
|
||||
Object.defineProperty(container, 'disabled', {
|
||||
get: () => disabled,
|
||||
set: (v) => {
|
||||
disabled = !!v;
|
||||
container.toggleAttribute('data-disabled', disabled);
|
||||
select.toggleAttribute('disabled', disabled);
|
||||
},
|
||||
})
|
||||
return container;
|
||||
}
|
||||
export function input(type: 'text' | 'number' | 'email' | 'password' | 'tel', settings?: { defaultValue?: string, change?: (value: string) => void, input?: (value: string) => void, focus?: () => void, blur?: () => void, class?: Class, disabled?: boolean }): HTMLInputElement
|
||||
{
|
||||
const input = dom("input", { attributes: { disabled: settings?.disabled }, class: [`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`, settings?.class], listeners: {
|
||||
input: () => settings?.input && settings.input(input.value),
|
||||
change: () => settings?.change && settings.change(input.value),
|
||||
focus: () => settings?.focus,
|
||||
blur: () => settings?.blur,
|
||||
}})
|
||||
if(settings?.defaultValue !== undefined) input.value = settings.defaultValue;
|
||||
|
||||
return input;
|
||||
}
|
||||
export function numberpicker(settings?: { defaultValue?: number, change?: (value: number) => void, input?: (value: number) => void, focus?: () => void, blur?: () => void, class?: Class, min?: number, max?: number, disabled?: boolean }): HTMLInputElement
|
||||
{
|
||||
let storedValue = settings?.defaultValue ?? 0;
|
||||
const validateAndChange = (value: number) => {
|
||||
if(isNaN(value))
|
||||
field.value = '';
|
||||
else
|
||||
{
|
||||
value = clamp(value, settings?.min ?? -Infinity, settings?.max ?? Infinity);
|
||||
field.value = value.toString(10);
|
||||
if(storedValue !== value)
|
||||
{
|
||||
storedValue = value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const field = dom("input", { attributes: { disabled: settings?.disabled }, 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`, settings?.class], listeners: {
|
||||
input: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.input && settings.input(storedValue),
|
||||
keydown: (e: KeyboardEvent) => {
|
||||
switch(e.key)
|
||||
{
|
||||
case "ArrowUp":
|
||||
validateAndChange(storedValue + (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "ArrowDown":
|
||||
validateAndChange(storedValue - (e.shiftKey ? 10 : 1)) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "PageUp":
|
||||
settings?.max && validateAndChange(settings.max) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
case "PageDown":
|
||||
settings?.min && validateAndChange(settings.min) && settings?.input && settings.input(storedValue);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
change: () => validateAndChange(parseInt(field.value.trim().toLowerCase().normalize().replace(/[a-z,.]/g, ""), 10)) && settings?.change && settings.change(storedValue),
|
||||
focus: () => settings?.focus && settings.focus(),
|
||||
blur: () => settings?.blur && settings.blur(),
|
||||
}});
|
||||
if(settings?.defaultValue) field.value = storedValue.toString(10);
|
||||
|
||||
return field;
|
||||
}
|
||||
|
|
@ -40,10 +40,11 @@ export type CharacterValues = {
|
|||
};
|
||||
export type CharacterConfig = {
|
||||
peoples: RaceConfig[],
|
||||
training: Record<MainStat, Record<TrainingLevel, TrainingOption[]>>;
|
||||
training: Record<MainStat, Record<TrainingLevel, string[]>>;
|
||||
abilities: Record<Ability, AbilityConfig>;
|
||||
spells: SpellConfig[];
|
||||
aspects: AspectConfig[];
|
||||
features: Record<string, Feature>;
|
||||
};
|
||||
export type SpellConfig = {
|
||||
id: string;
|
||||
|
|
@ -64,7 +65,7 @@ export type AbilityConfig = {
|
|||
export type RaceConfig = {
|
||||
name: string;
|
||||
description: string;
|
||||
options: Record<Level, Feature[]>;
|
||||
options: Record<Level, string[]>;
|
||||
};
|
||||
export type AspectConfig = {
|
||||
name: string;
|
||||
|
|
@ -80,23 +81,22 @@ export type AspectConfig = {
|
|||
};
|
||||
|
||||
export type FeatureEffect = {
|
||||
id: string;
|
||||
category: "value";
|
||||
operation: "add" | "set";
|
||||
property: string;
|
||||
value: number | `modifier/${MainStat}`;
|
||||
} | {
|
||||
category: "feature";
|
||||
kind: "action" | "reaction" | "freeaction" | "passive";
|
||||
text: string;
|
||||
value: number | `modifier/${MainStat}` | false;
|
||||
} | {
|
||||
id: string;
|
||||
category: "list";
|
||||
list: "spells";
|
||||
list: "spells" | "action" | "reaction" | "freeaction" | "passive";
|
||||
action: "add" | "remove";
|
||||
item: string;
|
||||
extra?: any;
|
||||
};
|
||||
export type FeatureItem = FeatureEffect | {
|
||||
category: "choice";
|
||||
id: string;
|
||||
category: "choice";
|
||||
settings?: { //If undefined, amount is 1 by default
|
||||
amount: number;
|
||||
exclusive: boolean; //Disallow to pick the same option twice
|
||||
|
|
@ -104,55 +104,26 @@ export type FeatureItem = FeatureEffect | {
|
|||
options: Array<FeatureEffect & { text: string }>;
|
||||
}
|
||||
export type Feature = {
|
||||
name?: string;
|
||||
id: string;
|
||||
description: string;
|
||||
effect: FeatureItem[];
|
||||
};
|
||||
|
||||
export type TrainingOption = {
|
||||
description: Array<{
|
||||
text: string;
|
||||
disposable?: boolean;
|
||||
replaced?: boolean;
|
||||
category?: Category;
|
||||
}>;
|
||||
|
||||
//Automatically calculated by compiler
|
||||
mana?: number;
|
||||
health?: number;
|
||||
speed?: false | number;
|
||||
initiative?: number;
|
||||
mastery?: keyof CompiledCharacter["mastery"];
|
||||
spellrank?: SpellType;
|
||||
defense?: Array<keyof CompiledCharacter["defense"]>;
|
||||
resistance?: Record<MainStat, number>;
|
||||
bonus?: Record<string, number>;
|
||||
spell?: string;
|
||||
|
||||
//Used during character creation, not used by compiler
|
||||
modifier?: number;
|
||||
ability?: number;
|
||||
spec?: number;
|
||||
spellslot?: number | MainStat;
|
||||
arts?: number | MainStat;
|
||||
|
||||
features?: FeatureItem[]; //TODO
|
||||
};
|
||||
export type CompiledCharacter = {
|
||||
id: number;
|
||||
owner?: number;
|
||||
username?: string;
|
||||
name: string;
|
||||
health: number;
|
||||
mana: number;
|
||||
health: number; //Max
|
||||
mana: number; //Max
|
||||
race: number;
|
||||
spellslots: number;
|
||||
artslots: number;
|
||||
spellslots: number; //Max
|
||||
artslots: number; //Max
|
||||
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;
|
||||
aspect: string;
|
||||
aspect: string; //ID
|
||||
speed: number | false;
|
||||
capacity: number | false;
|
||||
initiative: number;
|
||||
spells: string[];
|
||||
|
||||
values: CharacterValues,
|
||||
|
||||
|
|
@ -183,7 +154,7 @@ export type CompiledCharacter = {
|
|||
modifier: Record<MainStat, number>;
|
||||
abilities: Partial<Record<Ability, number>>;
|
||||
level: number;
|
||||
features: { [K in Extract<FeatureEffect, { category: "feature" }>["kind"]]?: string[] };
|
||||
lists: { [K in Extract<FeatureEffect, { category: "list" }>["list"]]?: string[] };
|
||||
|
||||
notes: string;
|
||||
};
|
||||
Loading…
Reference in New Issue