300 lines
22 KiB
Vue
300 lines
22 KiB
Vue
<script lang="ts">
|
|
function raceOptionToText(option: RaceOption): string
|
|
{
|
|
const text = [];
|
|
if(option.training) text.push(`+${option.training} point${option.training > 1 ? 's' : ''} de statistique${option.training > 1 ? 's' : ''}.`);
|
|
if(option.spec) text.push(`+${option.spec} spécialisation${option.spec > 1 ? 's' : ''}.`);
|
|
if(option.shaping) text.push(`+${option.shaping} transformation${option.shaping > 1 ? 's' : ''} par jour.`);
|
|
if(option.modifier) text.push(`+${option.modifier} au modifieur de votre choix.`);
|
|
if(option.health) text.push(`+${option.health} PV max.`);
|
|
if(option.mana) text.push(`+${option.mana} mana max.`);
|
|
return text.join('\n');
|
|
}
|
|
</script>
|
|
<script setup lang="ts">
|
|
import config from '#shared/character-config.json';
|
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
|
import PreviewA from '~/components/prose/PreviewA.vue';
|
|
import { clamp } from '~/shared/general.util';
|
|
import type { Character, CharacterConfig, Level, MainStat, RaceOption, TrainingLevel } from '~/types/character';
|
|
|
|
definePageMeta({
|
|
guestsGoesTo: '/user/login',
|
|
});
|
|
let id = useRouter().currentRoute.value.params.id;
|
|
const { add } = useToast();
|
|
const characterConfig = config as CharacterConfig;
|
|
const data = ref<Character>({
|
|
id: -1,
|
|
name: '',
|
|
progress: {
|
|
training: {
|
|
strength: [[1, 0], [2, 0], [3, 0], [4, 0]],
|
|
dexterity: [[1, 0], [2, 0], [3, 0], [4, 0]],
|
|
constitution: [[1, 0], [2, 0], [3, 0], [4, 0]],
|
|
intelligence: [[1, 0], [2, 0], [3, 0], [4, 0]],
|
|
curiosity: [[1, 0], [2, 0], [3, 0], [4, 0]],
|
|
charisma: [[1, 0], [2, 0], [3, 0], [4, 0]],
|
|
psyche: [[1, 0], [2, 0], [3, 0], [4, 0]],
|
|
},
|
|
level: 1,
|
|
race: {
|
|
index: undefined,
|
|
progress: [[1, 0]],
|
|
},
|
|
},
|
|
});
|
|
|
|
const peopleOpen = ref(false), trainingOpen = ref(false), abilityOpen = ref(false), trainingTab = ref(0);
|
|
const trainingPoints = computed(() => {
|
|
const options = data.value.progress.race.index !== undefined ? characterConfig.peoples[data.value.progress.race.index!].options : undefined;
|
|
return options ? data.value.progress.race.progress?.reduce((p, v) => {
|
|
return p + (options[v[0]][v[1]].training ?? 0)
|
|
}, 0) : 0;
|
|
});
|
|
const maxTraining = computed(() => Object.entries(data.value.progress.training).reduce((p, v) => { p[v[0]] = v[1].reduce((_p, _v) => Math.max(_p, _v[0]) , 0); return p; }, {} as Record<string, number>));
|
|
const trainingSpent = computed(() => Object.values(maxTraining.value).reduce((p, v) => p + v));
|
|
|
|
if(id !== 'new')
|
|
{
|
|
const character = await useRequestFetch()(`/api/character/${id}`);
|
|
|
|
if(!character)
|
|
{
|
|
throw new Error('Donnée du personnage introuvables');
|
|
}
|
|
|
|
data.value = { name: character.name, progress: character.progress } as Character;
|
|
}
|
|
|
|
function selectRaceOption(level: Level, choice: number)
|
|
{
|
|
const character = data.value;
|
|
if(level > character.progress.level)
|
|
return;
|
|
|
|
if(character.progress.race.progress === undefined)
|
|
character.progress.race.progress = [];
|
|
|
|
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
|
{
|
|
if(!character.progress.race.progress.some(e => e[0] == i))
|
|
return;
|
|
}
|
|
|
|
if(character.progress.race.progress.some(e => e[0] === level))
|
|
{
|
|
character.progress.race.progress.splice(character.progress.race.progress.findIndex(e => e[0] === level), 1, [level, choice]);
|
|
}
|
|
else
|
|
{
|
|
character.progress.race.progress.push([level, choice]);
|
|
}
|
|
|
|
data.value = character;
|
|
}
|
|
function switchTrainingOption(stat: MainStat, level: TrainingLevel, choice: number)
|
|
{
|
|
const character = data.value;
|
|
|
|
if(level == 0)
|
|
return;
|
|
|
|
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
|
{
|
|
if(!character.progress.training[stat].some(e => e[0] == i))
|
|
return;
|
|
}
|
|
|
|
if(character.progress.training[stat].some(e => e[0] === level))
|
|
{
|
|
if(character.progress.training[stat].some(e => e[0] === level && e[1] === choice))
|
|
{
|
|
for(let i = 15; i >= level; i --) //Invalidate higher levels
|
|
{
|
|
const index = character.progress.training[stat].findIndex(e => e[0] == i);
|
|
if(index !== -1)
|
|
character.progress.training[stat].splice(index, 1);
|
|
}
|
|
}
|
|
else
|
|
character.progress.training[stat].splice(character.progress.training[stat].findIndex(e => e[0] === level), 1, [level, choice]);
|
|
}
|
|
else if(trainingPoints.value && trainingPoints.value > 0)
|
|
{
|
|
character.progress.training[stat].push([level, choice]);
|
|
}
|
|
|
|
data.value = character;
|
|
}
|
|
function updateLevel()
|
|
{
|
|
const character = data.value;
|
|
|
|
if(character.progress.race.progress) //Invalidate higher levels
|
|
{
|
|
for(let level = 20; level > character.progress.level; level--)
|
|
{
|
|
const index = character.progress.race.progress.findIndex(e => e[0] == level);
|
|
if(index !== -1)
|
|
character.progress.race.progress.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
data.value = character;
|
|
}
|
|
async function save(leave: boolean)
|
|
{
|
|
if(id === 'new')
|
|
{
|
|
id = await useRequestFetch()(`/api/character`, {
|
|
method: 'post',
|
|
body: data.value,
|
|
onResponseError: (e) => {
|
|
add({ title: 'Erreur d\enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
|
}
|
|
});
|
|
add({ content: 'Personnage créé', type: 'success', duration: 25000, timer: true });
|
|
useRouter().replace({ name: 'character-id-edit', params: { id: id } })
|
|
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
|
}
|
|
else
|
|
{
|
|
await useRequestFetch()(`/api/character/${id}`, {
|
|
method: 'post',
|
|
body: data.value,
|
|
onResponseError: (e) => {
|
|
add({ title: 'Erreur d\enregistrement', content: e.response.status === 401 ? "Vous n'êtes pas autorisé à effectué cette opération" : e.response.statusText, type: 'error', duration: 25000, timer: true });
|
|
}
|
|
});
|
|
add({ content: 'Personnage enregistré', type: 'success', duration: 25000, timer: true });
|
|
if(leave) useRouter().push({ name: 'character-id', params: { id: id } });
|
|
}
|
|
}
|
|
|
|
useShortcuts({
|
|
"Meta_S": () =>save(false),
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Head>
|
|
<Title>d[any] - Edition de {{ data.name || 'nouveau personnage' }}</Title>
|
|
</Head>
|
|
<div class="flex flex-col gap-8 align-center">
|
|
<div class="flex flex-row gap-4 align-center justify-between">
|
|
<div></div>
|
|
<div class="flex flex-row gap-4 align-center justify-center">
|
|
<Tooltip side="left" message="Developpement en cours"><Avatar src="" icon="radix-icons:person" size="large" /></Tooltip>
|
|
<Label class="flex items-start justify-between flex-col gap-2">
|
|
<span class="pb-1 mx-2 md:p-0">Nom du personnage</span>
|
|
<input class="caret-light-50 dark:caret-dark-50 text-light-100 dark:text-dark-100 placeholder:text-light-50 dark:placeholder:text-dark-50
|
|
bg-light-20 dark:bg-dark-20 outline-none px-3 py-1 focus:shadow-raw transition-[box-shadow] focus:shadow-light-40 dark:focus:shadow-dark-40
|
|
border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20"
|
|
type="text" v-model="data.name">
|
|
</Label>
|
|
<Label class="flex items-start justify-between flex-col gap-2">
|
|
<span class="pb-1 mx-2 md:p-0">Niveau</span>
|
|
<NumberFieldRoot :min="1" :max="20" v-model="data.progress.level" @update:model-value="updateLevel" class="flex justify-center border border-light-35 dark:border-dark-35 bg-light-20 dark:bg-dark-20 data-[disabled]:border-light-20 dark:data-[disabled]:border-dark-20 data-[disabled]:bg-light-20 dark:data-[disabled]:bg-dark-20
|
|
data-[disabled]:text-light-70 dark:data-[disabled]:text-dark-70 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>
|
|
</div>
|
|
<div class="self-center">
|
|
<Tooltip side="right" message="Ctrl+S"><Button @click="() => save(true)">Enregistrer</Button></Tooltip>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-1 flex-col min-w-[800px] w-[75vw] max-w-[1200px]">
|
|
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="peopleOpen" @update:model-value="() => { trainingOpen = false; abilityOpen = false; }">
|
|
<template #label>
|
|
<span class="font-bold text-xl">Peuple</span>
|
|
</template>
|
|
<template #default>
|
|
<div class="m-2 overflow-auto">
|
|
<Select label="Peuple de votre personnage" :v-model="data.progress.race.index" @update:model-value="(index) => { data.progress.race.index = parseInt(index ?? '-1'); data.progress.race.progress = [[1, 0]]}">
|
|
<SelectItem v-for="(people, index) of characterConfig.peoples" :label="people.name" :value="index" :key="index" />
|
|
</Select>
|
|
<template v-if="data.progress.race.index !== undefined">
|
|
<div class="w-full border-b border-light-30 dark:border-dark-30 pb-4">
|
|
<span class="text-sm text-light-70 dark:text-dark-70">{{ characterConfig.peoples[data.progress.race.index].description }}</span>
|
|
</div>
|
|
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative">
|
|
<span class="sticky top-0 py-1 bg-light-0 dark:bg-dark-0 z-10 text-xl">Niveaux restants: {{ data.progress.level - (data.progress.race.progress?.length ?? 0) }}</span>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.peoples[data.progress.race.index].options" :class="{ 'opacity-30': index > data.progress.level }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-64" v-for="(option, i) of level" @click="selectRaceOption(index as Level, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= data.progress.level, '!border-accent-blue bg-accent-blue bg-opacity-20': data.progress.race.progress?.some(e => e[0] == index && e[1] === i) ?? false }"><MarkdownRenderer :content="raceOptionToText(option)" /></div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</Collapsible>
|
|
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="trainingOpen" :disabled="data.progress.race.index === undefined" @update:model-value="() => { peopleOpen = false; abilityOpen = false; }">
|
|
<template #label>
|
|
<span class="font-bold text-xl">Entrainement</span>
|
|
</template>
|
|
<template #default>
|
|
<div class="flex flex-col gap-4 max-h-[50vh] pe-4 relative overflow-y-auto overflow-x-hidden">
|
|
<div class="sticky top-0 py-2 bg-light-0 dark:bg-dark-0 z-10 flex justify-between">
|
|
<Icon icon="radix-icons:caret-left" class="w-6 h-6 border border-light-30 dark:border-dark-30 cursor-pointer" @click="() => trainingTab = clamp(trainingTab - 1, 0, 6)" />
|
|
<span class="text-xl" :class="{ 'text-light-red dark:text-dark-red': (trainingPoints ?? 0) < trainingSpent }">Points d'entrainement restants: {{ (trainingPoints ?? 0) - trainingSpent }}</span>
|
|
<Icon icon="radix-icons:caret-right" class="w-6 h-6 border border-light-30 dark:border-dark-30 cursor-pointer" @click="() => trainingTab = clamp(trainingTab + 1, 0, 6)" />
|
|
</div>
|
|
<div class="flex gap-4 relative" :style="`left: calc(calc(-100% - 1em) * ${trainingTab}); transition: left .5s ease;`">
|
|
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
|
|
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Force<span v-if="maxTraining.strength >= 0">: Niveau {{ maxTraining.strength }} (+{{ Math.floor(maxTraining.strength / 3) }})</span></div>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.strength" :class="{ 'opacity-30': index > maxTraining.strength + 1 }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('strength', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.strength + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.strength?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
|
</div>
|
|
</div>
|
|
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
|
|
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Dextérité<span v-if="maxTraining.dexterity >= 0">: Niveau {{ maxTraining.dexterity }} (+{{ Math.floor(maxTraining.dexterity / 3) }})</span></div>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.dexterity" :class="{ 'opacity-30': index > maxTraining.dexterity + 1 }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('dexterity', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.dexterity + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.dexterity?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
|
</div>
|
|
</div>
|
|
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
|
|
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Constitution<span v-if="maxTraining.constitution >= 0">: Niveau {{ maxTraining.constitution }} (+{{ Math.floor(maxTraining.constitution / 3) }})</span></div>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.constitution" :class="{ 'opacity-30': index > maxTraining.constitution + 1 }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('constitution', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.constitution + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.constitution?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
|
</div>
|
|
</div>
|
|
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
|
|
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Intelligence<span v-if="maxTraining.intelligence >= 0">: Niveau {{ maxTraining.intelligence }} (+{{ Math.floor(maxTraining.intelligence / 3) }})</span></div>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.intelligence" :class="{ 'opacity-30': index > maxTraining.intelligence + 1 }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('intelligence', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.intelligence + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.intelligence?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
|
</div>
|
|
</div>
|
|
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
|
|
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Curiosité<span v-if="maxTraining.curiosity >= 0">: Niveau {{ maxTraining.curiosity }} (+{{ Math.floor(maxTraining.curiosity / 3) }})</span></div>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.curiosity" :class="{ 'opacity-30': index > maxTraining.curiosity + 1 }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('curiosity', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.curiosity + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.curiosity?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
|
</div>
|
|
</div>
|
|
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
|
|
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Charisme<span v-if="maxTraining.charisma >= 0">: Niveau {{ maxTraining.charisma }} (+{{ Math.floor(maxTraining.charisma / 3) }})</span></div>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.charisma" :class="{ 'opacity-30': index > maxTraining.charisma + 1 }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('charisma', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.charisma + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.charisma?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
|
</div>
|
|
</div>
|
|
<div class="flex w-full flex-shrink-0 flex-col gap-2 relative">
|
|
<div class="sticky top-1 left-16 py-1 px-3 bg-light-0 dark:bg-dark-0 z-10 text-xl font-bold self-start border border-light-30 dark:border-dark-30">Psyché<span v-if="maxTraining.psyche >= 0">: Niveau {{ maxTraining.psyche }} (+{{ Math.floor(maxTraining.psyche / 3) }})</span></div>
|
|
<div class="flex flex-row gap-4 justify-center" v-for="(level, index) of characterConfig.training.psyche" :class="{ 'opacity-30': index > maxTraining.psyche + 1 }">
|
|
<div class="border border-light-40 dark:border-dark-40 cursor-pointer px-2 py-1 w-1/3" v-for="(option, i) of level" @click="switchTrainingOption('psyche', index as TrainingLevel, i)" :class="{ 'hover:border-light-60 dark:hover:border-dark-60': index <= maxTraining.psyche + 1, '!border-accent-blue bg-accent-blue bg-opacity-20': index == 0 || (data.progress.training.psyche?.some(e => e[0] == index && e[1] === i) ?? false) }"><MarkdownRenderer :proses="{ 'a': PreviewA }" :content="option.description.map(e => e.text).join('\n')" /></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Collapsible>
|
|
<Collapsible class="border-b border-light-30 dark:border-dark-30 p-1" v-model="abilityOpen" :disabled="data.progress.race.index === undefined" @update:model-value="() => { trainingOpen = false; peopleOpen = false; }">
|
|
<template #label>
|
|
<span class="font-bold text-xl">Compétences</span>
|
|
</template>
|
|
<template #default>
|
|
|
|
</template>
|
|
</Collapsible>
|
|
</div>
|
|
</div>
|
|
</template> |