Add CharacterBuilder class to unify and compile the features
This commit is contained in:
parent
996b9711e4
commit
a8dcc47a1b
|
|
@ -18,7 +18,7 @@ const { message, delay = 300, side } = defineProps<{
|
|||
delay?: number
|
||||
disabled?: boolean
|
||||
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||
align: 'start' | 'center' | 'end'
|
||||
align?: 'start' | 'center' | 'end'
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<template v-if="model && model.people !== undefined">
|
||||
<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.level" @update:model-value="updateLevel" 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">
|
||||
<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>
|
||||
|
|
@ -28,13 +28,13 @@
|
|||
<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.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.level }"></div><span class="sticky top-0">{{ index }}</span></div>
|
||||
<div class="flex flex-row gap-4 justify-center" :class="{ 'opacity-30': index > model.level }">
|
||||
<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="chooseOption(parseInt(index as unknown as string, 10) as Level, i)"
|
||||
:class="{ 'hover:border-light-70 dark:hover:border-dark-70 cursor-pointer': index <= model.level, '!border-accent-blue bg-accent-blue bg-opacity-20': model.leveling?.some(e => e[0] == index && e[1] === i) ?? false }">
|
||||
<span class="text-wrap whitespace-pre">{{ raceOptionToText(option) }}</span>
|
||||
<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>
|
||||
|
|
@ -44,70 +44,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Character, CharacterConfig, Level, RaceOption } from '~/types/character';
|
||||
import type { CharacterBuilder } from '~/shared/character';
|
||||
import type { CharacterConfig, Level } from '~/types/character';
|
||||
|
||||
const { config } = defineProps<{
|
||||
config: CharacterConfig,
|
||||
}>();
|
||||
const model = defineModel<Character>({ required: true });
|
||||
const model = defineModel<CharacterBuilder>({ required: true });
|
||||
|
||||
const emit = defineEmits(['next']);
|
||||
|
||||
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.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.abilities) text.push(`+${option.abilities} point${option.abilities > 1 ? 's' : ''} de compétence${option.abilities > 1 ? 's' : ''}.`);
|
||||
if(option.health) text.push(`+${option.health} PV max.`);
|
||||
if(option.mana) text.push(`+${option.mana} mana max.`);
|
||||
if(option.spellslots) text.push(`+${option.spellslots} sort${option.spellslots > 1 ? 's' : ''} maitrisé${option.spellslots > 1 ? 's' : ''}.`);
|
||||
return text.join('\n');
|
||||
}
|
||||
function chooseOption(level: Level, choice: number)
|
||||
{
|
||||
const character = model.value;
|
||||
if(level > character.level)
|
||||
return;
|
||||
|
||||
if(character.leveling === undefined)
|
||||
character.leveling = [[1, 0]];
|
||||
|
||||
if(level == 1)
|
||||
return;
|
||||
|
||||
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
||||
{
|
||||
if(!character.leveling.some(e => e[0] == i))
|
||||
return;
|
||||
}
|
||||
|
||||
if(character.leveling.some(e => e[0] == level))
|
||||
{
|
||||
character.leveling.splice(character.leveling.findIndex(e => e[0] == level), 1, [level, choice]);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.leveling.push([level, choice]);
|
||||
}
|
||||
|
||||
model.value = character;
|
||||
}
|
||||
function updateLevel()
|
||||
{
|
||||
const character = model.value;
|
||||
|
||||
if(character.leveling) //Invalidate higher levels
|
||||
{
|
||||
for(let level = 20; level > character.level; level--)
|
||||
{
|
||||
const index = character.leveling.findIndex(e => e[0] == level);
|
||||
if(index !== -1)
|
||||
character.leveling.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
model.value = character;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
<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.name" class="flex-none"/>
|
||||
<Switch label="Privé ?" :default-value="model.visibility === 'private'" @update:model-value="(e) => model!.visibility = e ? 'private' : 'public'" />
|
||||
<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.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.people === i }">
|
||||
<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>
|
||||
|
|
@ -18,12 +18,13 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Character, CharacterConfig } from '~/types/character';
|
||||
import type { CharacterBuilder } from '~/shared/character';
|
||||
import type { CharacterConfig } from '~/types/character';
|
||||
|
||||
const { config } = defineProps<{
|
||||
config: CharacterConfig,
|
||||
}>();
|
||||
const model = defineModel<Character>();
|
||||
const model = defineModel<CharacterBuilder>();
|
||||
|
||||
const emit = defineEmits(['next']);
|
||||
</script>
|
||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
import { relations } from 'drizzle-orm';
|
||||
import { int, text, sqliteTable, primaryKey, blob } from 'drizzle-orm/sqlite-core';
|
||||
import { ABILITIES, MAIN_STATS } from '../types/character';
|
||||
import { ABILITIES, MAIN_STATS } from '~/shared/character';
|
||||
|
||||
export const usersTable = sqliteTable("users", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import config from '#shared/character-config.json';
|
||||
import { ABILITIES, defaultCharacter, MAIN_STATS, type Ability, type Character, type CharacterConfig, type MainStat } from '~/types/character';
|
||||
import characterConfig from '#shared/character-config.json';
|
||||
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||
import { CharacterBuilder, defaultCharacter } from '~/shared/character';
|
||||
import type { Character, CharacterConfig } from '~/types/character';
|
||||
|
||||
export interface EditorValues
|
||||
{
|
||||
health: number,
|
||||
mana: number,
|
||||
training: number,
|
||||
trainingSpent: number,
|
||||
abilities: Record<Ability, number>,
|
||||
abilitiesMax: Record<Ability, number>,
|
||||
modifiers: Record<MainStat, number>,
|
||||
}
|
||||
|
||||
const stepTexts = {
|
||||
const stepTexts: Record<number, string> = {
|
||||
0: 'Choisissez un peuple afin de définir la progression de votre personnage au fil des niveaux.',
|
||||
1: 'Déterminez la progression de votre personnage en choisissant une option par niveau disponible.',
|
||||
2: 'Spécialisez votre personnage en attribuant vos points d\'entrainement parmi les 7 branches disponibles.\nChaque paliers de 3 points augmentent votre modifieur.',
|
||||
|
|
@ -27,21 +17,12 @@ definePageMeta({
|
|||
});
|
||||
let id = useRouter().currentRoute.value.params.id;
|
||||
const { add } = useToast();
|
||||
const characterConfig = config as CharacterConfig;
|
||||
const config = characterConfig as CharacterConfig;
|
||||
const data = ref<Character>({ ...defaultCharacter });
|
||||
const builder = markRaw(new CharacterBuilder(data.value));
|
||||
|
||||
const step = ref(0);
|
||||
|
||||
const values: EditorValues = reactive({
|
||||
health: 0,
|
||||
mana: 0,
|
||||
training: 0,
|
||||
trainingSpent: 0,
|
||||
abilities: Object.fromEntries(ABILITIES.map(e => [e, 0])) as Record<Ability, number>,
|
||||
abilitiesMax: Object.fromEntries(ABILITIES.map(e => [e, 0])) as Record<Ability, number>,
|
||||
modifiers: Object.fromEntries(MAIN_STATS.map(e => [e, 0])) as Record<MainStat, number>,
|
||||
});
|
||||
|
||||
if(id !== 'new')
|
||||
{
|
||||
const character = await useRequestFetch()(`/api/character/${id}`);
|
||||
|
|
@ -115,20 +96,20 @@ useShortcuts({
|
|||
</div>
|
||||
</div>
|
||||
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 0">
|
||||
<PeopleSelector v-model="data" :config="characterConfig" @next="step = 1" />
|
||||
<PeopleSelector v-model="builder" :config="config" @next="step = 1" />
|
||||
</div>
|
||||
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 1">
|
||||
<LevelEditor v-model="data" :config="characterConfig" @next="step = 2" />
|
||||
<LevelEditor v-model="builder" :config="config" @next="step = 2" />
|
||||
</div>
|
||||
<div class="flex-1 outline-none max-w-full w-full h-full max-h-full overflow-y-auto" v-show="step === 2">
|
||||
<TrainingEditor v-model="data" :config="characterConfig" @next="step = 3" />
|
||||
<!-- <div class="flex-1 outline-none max-w-full w-full h-full max-h-full overflow-y-auto" v-show="step === 2">
|
||||
<TrainingEditor v-model="builder" :config="config" @next="step = 3" />
|
||||
</div>
|
||||
<div class="flex-1 outline-none max-w-full w-fulloverflow-y-auto" v-show="step === 3">
|
||||
<AbilityEditor v-model="data" :config="characterConfig" @next="step = 4" />
|
||||
<AbilityEditor v-model="builder" :config="config" @next="step = 4" />
|
||||
</div>
|
||||
<div class="flex-1 outline-none max-w-full w-full overflow-y-auto" v-show="step === 4">
|
||||
<AspectSelector v-model="data" :config="characterConfig" @next="save(true)" />
|
||||
</div>
|
||||
<AspectSelector v-model="builder" :config="config" @next="save(true)" />
|
||||
</div> -->
|
||||
</StepperRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation, type Ability, type DoubleIndex, type MainStat, type TrainingLevel } from '~/types/character';
|
||||
import { CharacterValidation } from '~/shared/character';
|
||||
import { type Ability, type MainStat } from '~/types/character';
|
||||
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
import useDatabase from '~/composables/useDatabase';
|
||||
import { characterAbilitiesTable, characterLevelingTable, characterModifiersTable, characterSpellsTable, characterTable, characterTrainingTable } from '~/db/schema';
|
||||
import { CharacterValidation, type Ability, type MainStat } from '~/types/character';
|
||||
import { CharacterValidation } from '~/shared/character';
|
||||
import { type Ability, type MainStat } from '~/types/character';
|
||||
|
||||
export default defineEventHandler(async (e) => {
|
||||
const params = getRouterParam(e, "id");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import useDatabase from '~/composables/useDatabase';
|
||||
import { defaultCharacter, type Ability, type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Feature, type FeatureItem, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
|
||||
import { type Character, type CharacterConfig, type CompiledCharacter, type DoubleIndex, type Level, type MainStat, type TrainingLevel, type TrainingOption } from '~/types/character';
|
||||
import characterData from '#shared/character-config.json';
|
||||
import { group } from '~/shared/general.util';
|
||||
import { defaultCharacter, MAIN_STATS } from '~/shared/character';
|
||||
|
||||
export default defineCachedEventHandler(async (e) => {
|
||||
const id = getRouterParam(e, "id");
|
||||
|
|
@ -80,7 +81,7 @@ function compileCharacter(character: Character & { username?: string }): Compile
|
|||
action: [],
|
||||
reaction: [],
|
||||
freeaction: [],
|
||||
misc: [],
|
||||
passive: [],
|
||||
},
|
||||
abilities: {
|
||||
athletics: 0,
|
||||
|
|
@ -130,90 +131,17 @@ function compileCharacter(character: Character & { username?: string }): Compile
|
|||
magicelement: 0,
|
||||
magicinstinct: 0,
|
||||
},
|
||||
resistance: {
|
||||
stun: [0, 0],
|
||||
bleed: [0, 0],
|
||||
poison: [0, 0],
|
||||
fear: [0, 0],
|
||||
influence: [0, 0],
|
||||
charm: [0, 0],
|
||||
possesion: [0, 0],
|
||||
precision: [0, 0],
|
||||
knowledge: [0, 0],
|
||||
instinct: [0, 0]
|
||||
},
|
||||
resistance: {},//Object.fromEntries(MAIN_STATS.map(e => [e as MainStat, [0, 0]])) as Record<MainStat, [number, number]>,
|
||||
initiative: 0,
|
||||
aspect: "",
|
||||
notes: character.notes ?? "",
|
||||
};
|
||||
|
||||
features.forEach(e => e[1].forEach(_e => _e.features?.forEach(f => applyFeature(compiled, f))));
|
||||
//features.forEach(e => e[1].forEach(_e => _e.features?.forEach(f => applyFeature(compiled, f))));
|
||||
|
||||
return compiled;
|
||||
}
|
||||
function applyFeature(character: CompiledCharacter, f: FeatureItem)
|
||||
{
|
||||
switch(f.category)
|
||||
{
|
||||
case "action":
|
||||
character.features.action.push(f.text);
|
||||
|
||||
return;
|
||||
case "reaction":
|
||||
character.features.reaction.push(f.text);
|
||||
|
||||
return;
|
||||
case "freeaction":
|
||||
character.features.freeaction.push(f.text);
|
||||
|
||||
return;
|
||||
case "misc":
|
||||
character.features.misc.push(f.text);
|
||||
|
||||
return;
|
||||
case "asset":
|
||||
if(f.type === 'add')
|
||||
character[f.kind].push(f.asset);
|
||||
else
|
||||
character[f.kind] = character[f.kind].filter(e => e !== f.asset);
|
||||
|
||||
return;
|
||||
case "value":
|
||||
const path = f.property.split(".");
|
||||
const object = path.slice(0, -1).reduce((p, v) => p[v], character as any);
|
||||
|
||||
switch(f.type)
|
||||
{
|
||||
case "add":
|
||||
if(!['number'].includes(typeof object[path[path.length - 1]]))
|
||||
break;
|
||||
|
||||
object[path[path.length - 1]] += f.value;
|
||||
|
||||
break;
|
||||
case "remove":
|
||||
if(!['number'].includes(typeof object[path[path.length - 1]]))
|
||||
break;
|
||||
|
||||
object[path[path.length - 1]] -= f.value as number;
|
||||
|
||||
break;
|
||||
case "set":
|
||||
if(!['number', 'boolean'].includes(typeof object[path[path.length - 1]]))
|
||||
break;
|
||||
|
||||
object[path[path.length - 1]] = f.value;
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
export function getFeaturesOf(stat: MainStat, progression: DoubleIndex<TrainingLevel>[]): TrainingOption[]
|
||||
{
|
||||
const config = characterData as CharacterConfig;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
import type { Ability, Character, CharacterConfig, CompiledCharacter, DoubleIndex, Feature, FeatureItem, Level, MainStat, SpellElement, SpellType, TrainingLevel } from "~/types/character";
|
||||
import { z, type ZodRawShape } from "zod/v4";
|
||||
import characterConfig from './character-config.json';
|
||||
|
||||
const config = characterConfig as CharacterConfig;
|
||||
|
||||
export const MAIN_STATS = ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] as const;
|
||||
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const;
|
||||
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const;
|
||||
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const;
|
||||
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const;
|
||||
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const;
|
||||
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const;
|
||||
|
||||
export const defaultCharacter: Character = {
|
||||
id: -1,
|
||||
|
||||
name: "",
|
||||
people: undefined,
|
||||
level: 1,
|
||||
health: 0,
|
||||
mana: 0,
|
||||
|
||||
training: MAIN_STATS.reduce((p, v) => { p[v] = [[0, 0]]; return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
|
||||
leveling: [[1, 0]],
|
||||
abilities: {},
|
||||
spells: [],
|
||||
modifiers: {},
|
||||
choices: {},
|
||||
|
||||
owner: -1,
|
||||
visibility: "private",
|
||||
};
|
||||
|
||||
export const mainStatTexts: Record<MainStat, string> = {
|
||||
"strength": "Force",
|
||||
"dexterity": "Dextérité",
|
||||
"constitution": "Constitution",
|
||||
"intelligence": "Intelligence",
|
||||
"curiosity": "Curiosité",
|
||||
"charisma": "Charisme",
|
||||
"psyche": "Psyché",
|
||||
};
|
||||
|
||||
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' },
|
||||
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
|
||||
thunder: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Foudre' },
|
||||
earth: { class: 'text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange', text: 'Terre' },
|
||||
arcana: { class: 'text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo', text: 'Arcane' },
|
||||
air: { class: 'text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime', text: 'Air' },
|
||||
nature: { class: 'text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green', text: 'Nature' },
|
||||
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
|
||||
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
|
||||
};
|
||||
|
||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||
|
||||
export const CharacterValidation = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
people: z.number().nullable(),
|
||||
level: z.number().min(1).max(20),
|
||||
aspect: z.number().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
health: z.number().default(0),
|
||||
mana: z.number().default(0),
|
||||
training: z.object(MAIN_STATS.reduce((p, v) => {
|
||||
p[v] = z.array(z.tuple([z.number().min(0).max(15), z.number()]));
|
||||
return p;
|
||||
}, {} as Record<MainStat, z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>)),
|
||||
leveling: z.array(z.tuple([z.number().min(1).max(20), z.number()])),
|
||||
abilities: z.object(ABILITIES.reduce((p, v) => {
|
||||
p[v] = z.tuple([z.number(), z.number()]);
|
||||
return p;
|
||||
}, {} as Record<Ability, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>)).partial(),
|
||||
spells: z.string().array(),
|
||||
modifiers: z.object(MAIN_STATS.reduce((p, v) => {
|
||||
p[v] = z.number();
|
||||
return p;
|
||||
}, {} as Record<MainStat, z.ZodNumber>)).partial(),
|
||||
owner: z.number(),
|
||||
username: z.string().optional(),
|
||||
visibility: z.enum(["public", "private"]),
|
||||
thumbnail: z.any(),
|
||||
});
|
||||
|
||||
type PropertySum = { list: Array<string | number>, value: number, _dirty: boolean };
|
||||
export class CharacterBuilder
|
||||
{
|
||||
private _character: Character;
|
||||
private _result!: CompiledCharacter;
|
||||
private _buffer: Record<string, PropertySum> = {};
|
||||
|
||||
constructor(character: Character)
|
||||
{
|
||||
this._character = character;
|
||||
|
||||
if(character.people)
|
||||
{
|
||||
const people = config.peoples[character.people];
|
||||
|
||||
character.leveling.forEach(e => {
|
||||
const feature = people.options[e[0]][e[1]];
|
||||
feature.effect.map(e => this.apply(e));
|
||||
});
|
||||
|
||||
MAIN_STATS.forEach(stat => {
|
||||
character.training[stat].forEach(option => {
|
||||
config.training[stat][option[0]][option[1]].features?.forEach(this.apply.bind(this));
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
compile(properties: string[])
|
||||
{
|
||||
const queue = properties;
|
||||
queue.forEach(e => {
|
||||
const buffer = this._buffer[e];
|
||||
|
||||
if(buffer._dirty === true)
|
||||
{
|
||||
let sum = 0;
|
||||
for(let i = 0; i < buffer.list.length; i++)
|
||||
{
|
||||
if(typeof buffer.list[i] === 'string')
|
||||
{
|
||||
if(this._buffer[buffer.list[i]]._dirty)
|
||||
{
|
||||
//Put it back in queue since its dependencies haven't been resolved yet
|
||||
queue.push(e);
|
||||
return;
|
||||
}
|
||||
else
|
||||
sum += this._buffer[buffer.list[i]].value;
|
||||
}
|
||||
else
|
||||
sum += buffer.list[i] as number;
|
||||
}
|
||||
|
||||
const path = e[0].split("/");
|
||||
const object = path.slice(0, -1).reduce((p, v) => p[v], this._result as any);
|
||||
|
||||
object[path.slice(-1)[0]] = sum;
|
||||
this._buffer[e].value = sum;
|
||||
this._buffer[e]._dirty = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
updateLevel(level: Level)
|
||||
{
|
||||
this._character.level = level;
|
||||
|
||||
if(this._character.leveling) //Invalidate higher levels
|
||||
{
|
||||
for(let level = 20; level > this._character.level; level--)
|
||||
{
|
||||
const index = this._character.leveling.findIndex(e => e[0] == level);
|
||||
if(index !== -1)
|
||||
{
|
||||
const option = this._character.leveling[level];
|
||||
this._character.leveling.splice(index, 1);
|
||||
|
||||
this.remove(config.peoples[this._character.people!].options[option[0]][option[1]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
toggleLevelOption(level: Level, choice: number)
|
||||
{
|
||||
if(level > this._character.level) //Cannot add more level options than the current level
|
||||
return;
|
||||
|
||||
if(this._character.leveling === undefined) //Add level 1 if missing
|
||||
{
|
||||
this._character.leveling = [[1, 0]];
|
||||
this.add(config.peoples[this._character.people!].options[1][0]);
|
||||
}
|
||||
|
||||
if(level == 1) //Cannot remove level 1
|
||||
return;
|
||||
|
||||
for(let i = 1; i < level; i++) //Check previous levels as a requirement
|
||||
{
|
||||
if(!this._character.leveling.some(e => e[0] == i))
|
||||
return;
|
||||
}
|
||||
|
||||
const option = this._character.leveling.find(e => e[0] == level);
|
||||
if(option && option[1] !== choice) //If the given level is already selected, switch to the new choice
|
||||
{
|
||||
this._character.leveling.splice(this._character.leveling.findIndex(e => e[0] == level), 1, [level, choice]);
|
||||
|
||||
this.remove(config.peoples[this._character.people!].options[option[0]][option[1]]);
|
||||
this.add(config.peoples[this._character.people!].options[level][choice]);
|
||||
}
|
||||
else if(!option)
|
||||
{
|
||||
this._character.leveling.push([level, choice]);
|
||||
|
||||
this.add(config.peoples[this._character.people!].options[level][choice]);
|
||||
}
|
||||
}
|
||||
toggleTrainingOption(stat: MainStat, level: TrainingLevel, option: number)
|
||||
{
|
||||
|
||||
}
|
||||
private add(feature: Feature)
|
||||
{
|
||||
feature.effect.forEach(this.apply.bind(this));
|
||||
}
|
||||
private remove(feature: Feature)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
get character(): Character
|
||||
{
|
||||
return this._character;
|
||||
}
|
||||
get compiled(): CompiledCharacter
|
||||
{
|
||||
this.compile(Object.keys(this._buffer));
|
||||
|
||||
return this._result;
|
||||
}
|
||||
get values(): Record<string, number>
|
||||
{
|
||||
const keys = Object.keys(this._buffer);
|
||||
this.compile(keys);
|
||||
|
||||
return keys.reduce((p, v) => {
|
||||
p[v] = this._buffer[v].value;
|
||||
return p;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
|
||||
private apply(feature: FeatureItem)
|
||||
{
|
||||
switch(feature.category)
|
||||
{
|
||||
case "feature":
|
||||
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);
|
||||
else
|
||||
this._result[feature.list] = this._result[feature.list].filter((e: string) => e !== feature.item);
|
||||
|
||||
return;
|
||||
case "value":
|
||||
this._buffer[feature.property] ??= { list: [], value: 0, _dirty: true };
|
||||
|
||||
if(feature.operation === 'add')
|
||||
this._buffer[feature.property].list.push(feature.value);
|
||||
else if(feature.operation === 'set')
|
||||
this._buffer[feature.property].list = [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]));
|
||||
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { CanvasContent, CanvasNode } from "~/types/canvas";
|
||||
import type { CanvasNode } from "~/types/canvas";
|
||||
import type { CanvasPreferences } from "~/types/general";
|
||||
import type { Position, Box, Direction } from "./canvas.util";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
import type { MAIN_STATS, ABILITIES, LEVELS, TRAINING_LEVELS, SPELL_TYPES, CATEGORIES, SPELL_ELEMENTS } from "~/shared/character";
|
||||
|
||||
export type MainStat = typeof MAIN_STATS[number];
|
||||
export type Ability = typeof ABILITIES[number];
|
||||
export type Level = typeof LEVELS[number];
|
||||
export type TrainingLevel = typeof TRAINING_LEVELS[number];
|
||||
export type SpellType = typeof SPELL_TYPES[number];
|
||||
export type Category = typeof CATEGORIES[number];
|
||||
export type SpellElement = typeof SPELL_ELEMENTS[number];
|
||||
|
||||
export type DoubleIndex<T extends number | string> = [T, number];
|
||||
|
||||
export type Character = {
|
||||
id: number;
|
||||
|
||||
name: string;
|
||||
people?: number;
|
||||
level: number;
|
||||
aspect?: number;
|
||||
notes?: string | null;
|
||||
health: number;
|
||||
mana: number;
|
||||
|
||||
training: Record<MainStat, DoubleIndex<TrainingLevel>[]>;
|
||||
leveling: DoubleIndex<Level>[];
|
||||
abilities: Partial<Record<Ability, [number, number]>>; //First is the ability, second is the max increment
|
||||
spells: string[]; //Spell ID
|
||||
modifiers: Partial<Record<MainStat, number>>;
|
||||
|
||||
choices: Record<string, number[]>;
|
||||
|
||||
owner: number;
|
||||
username?: string;
|
||||
visibility: "private" | "public";
|
||||
};
|
||||
export type CharacterValues = {
|
||||
health: number;
|
||||
mana: number;
|
||||
};
|
||||
export type CharacterConfig = {
|
||||
peoples: Race[],
|
||||
training: Record<MainStat, Record<TrainingLevel, TrainingOption[]>>;
|
||||
abilities: Record<Ability, AbilityConfig>;
|
||||
spells: SpellConfig[];
|
||||
};
|
||||
export type SpellConfig = {
|
||||
id: string;
|
||||
name: string;
|
||||
rank: 1 | 2 | 3 | 4;
|
||||
type: SpellType;
|
||||
cost: number;
|
||||
speed: "action" | "reaction" | number;
|
||||
elements: Array<SpellElement>;
|
||||
effect: string;
|
||||
tags?: string[];
|
||||
};
|
||||
export type AbilityConfig = {
|
||||
max: [MainStat, MainStat];
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
export type Race = {
|
||||
name: string;
|
||||
description: string;
|
||||
options: Record<Level, Feature[]>;
|
||||
};
|
||||
|
||||
export type FeatureEffect = {
|
||||
category: "value";
|
||||
operation: "add" | "set";
|
||||
property: string;
|
||||
value: number | `modifier/${MainStat}`;
|
||||
} | {
|
||||
category: "feature";
|
||||
kind: "action" | "reaction" | "freeaction" | "passive";
|
||||
text: string;
|
||||
} | {
|
||||
category: "list";
|
||||
list: "spells";
|
||||
action: "add" | "remove";
|
||||
item: string;
|
||||
};
|
||||
export type FeatureItem = FeatureEffect | {
|
||||
category: "choice";
|
||||
id: string;
|
||||
settings?: { //If undefined, amount is 1 by default
|
||||
amount: number;
|
||||
exclusive: boolean; //Disallow to pick the same option twice
|
||||
};
|
||||
options: FeatureEffect[];
|
||||
}
|
||||
export type Feature = {
|
||||
name?: 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;
|
||||
race: number;
|
||||
spellslots: number;
|
||||
artslots: number;
|
||||
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;
|
||||
aspect: string;
|
||||
speed: number | false;
|
||||
initiative: number;
|
||||
spells: string[];
|
||||
|
||||
values: CharacterValues,
|
||||
|
||||
defense: {
|
||||
hardcap: number;
|
||||
static: number;
|
||||
activeparry: number;
|
||||
activedodge: number;
|
||||
passiveparry: number;
|
||||
passivedodge: number;
|
||||
};
|
||||
|
||||
mastery: {
|
||||
strength: number;
|
||||
dexterity: number;
|
||||
shield: number;
|
||||
armor: number;
|
||||
multiattack: number;
|
||||
magicpower: number;
|
||||
magicspeed: number;
|
||||
magicelement: number;
|
||||
magicinstinct: number;
|
||||
};
|
||||
|
||||
bonus: Record<string, number>; //Any special bonus goes here
|
||||
resistance: Record<string, number>;
|
||||
|
||||
modifier: Record<MainStat, number>;
|
||||
abilities: Partial<Record<Ability, number>>;
|
||||
level: number;
|
||||
features: { [K in Extract<FeatureEffect, { category: "feature" }>["kind"]]: string[] };
|
||||
|
||||
notes: string;
|
||||
};
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
import { z, type ZodRawShape } from "zod/v4";
|
||||
|
||||
export const MAIN_STATS = ["strength","dexterity","constitution","intelligence","curiosity","charisma","psyche"] as const; export type MainStat = typeof MAIN_STATS[number];
|
||||
export const ABILITIES = ["athletics","acrobatics","intimidation","sleightofhand","stealth","survival","investigation","history","religion","arcana","understanding","perception","performance","medecine","persuasion","animalhandling","deception"] as const; export type Ability = typeof ABILITIES[number];
|
||||
export const LEVELS = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20] as const; export type Level = typeof LEVELS[number];
|
||||
export const TRAINING_LEVELS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] as const; export type TrainingLevel = typeof TRAINING_LEVELS[number];
|
||||
export const SPELL_TYPES = ["precision","knowledge","instinct","arts"] as const; export type SpellType = typeof SPELL_TYPES[number];
|
||||
export const CATEGORIES = ["action","reaction","freeaction","misc"] as const; export type Category = typeof CATEGORIES[number];
|
||||
export const SPELL_ELEMENTS = ["fire","ice","thunder","earth","arcana","air","nature","light","psyche"] as const; export type SpellElement = typeof SPELL_ELEMENTS[number];
|
||||
|
||||
export type DoubleIndex<T extends number | string> = [T, number];
|
||||
|
||||
export const defaultCharacter: Character = {
|
||||
id: -1,
|
||||
|
||||
name: "",
|
||||
people: undefined,
|
||||
level: 1,
|
||||
health: 0,
|
||||
mana: 0,
|
||||
|
||||
training: MAIN_STATS.reduce((p, v) => { p[v] = [[0, 0]]; return p; }, {} as Record<MainStat, DoubleIndex<TrainingLevel>[]>),
|
||||
leveling: [[1, 0]],
|
||||
abilities: {},
|
||||
spells: [],
|
||||
modifiers: {},
|
||||
|
||||
owner: -1,
|
||||
visibility: "private",
|
||||
};
|
||||
export const mainStatTexts: Record<MainStat, string> = {
|
||||
"strength": "Force",
|
||||
"dexterity": "Dextérité",
|
||||
"constitution": "Constitution",
|
||||
"intelligence": "Intelligence",
|
||||
"curiosity": "Curiosité",
|
||||
"charisma": "Charisme",
|
||||
"psyche": "Psyché",
|
||||
};
|
||||
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' },
|
||||
ice: { class: 'text-light-blue dark:text-dark-blue border-light-blue dark:border-dark-blue bg-light-blue dark:bg-dark-blue', text: 'Glace' },
|
||||
thunder: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Foudre' },
|
||||
earth: { class: 'text-light-orange dark:text-dark-orange border-light-orange dark:border-dark-orange bg-light-orange dark:bg-dark-orange', text: 'Terre' },
|
||||
arcana: { class: 'text-light-indigo dark:text-dark-indigo border-light-indigo dark:border-dark-indigo bg-light-indigo dark:bg-dark-indigo', text: 'Arcane' },
|
||||
air: { class: 'text-light-lime dark:text-dark-lime border-light-lime dark:border-dark-lime bg-light-lime dark:bg-dark-lime', text: 'Air' },
|
||||
nature: { class: 'text-light-green dark:text-dark-green border-light-green dark:border-dark-green bg-light-green dark:bg-dark-green', text: 'Nature' },
|
||||
light: { class: 'text-light-yellow dark:text-dark-yellow border-light-yellow dark:border-dark-yellow bg-light-yellow dark:bg-dark-yellow', text: 'Lumière' },
|
||||
psyche: { class: 'text-light-purple dark:text-dark-purple border-light-purple dark:border-dark-purple bg-light-purple dark:bg-dark-purple', text: 'Psy' },
|
||||
};
|
||||
export const spellTypeTexts: Record<SpellType, string> = { "instinct": "Instinct", "knowledge": "Savoir", "precision": "Précision", "arts": "Oeuvres" };
|
||||
|
||||
export const CharacterValidation = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
people: z.number().nullable(),
|
||||
level: z.number().min(1).max(20),
|
||||
aspect: z.number().nullable().optional(),
|
||||
notes: z.string().nullable().optional(),
|
||||
health: z.number().default(0),
|
||||
mana: z.number().default(0),
|
||||
training: z.object(MAIN_STATS.reduce((p, v) => {
|
||||
p[v] = z.array(z.tuple([z.number().min(0).max(15), z.number()]));
|
||||
return p;
|
||||
}, {} as Record<MainStat, z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>>)),
|
||||
leveling: z.array(z.tuple([z.number().min(1).max(20), z.number()])),
|
||||
abilities: z.object(ABILITIES.reduce((p, v) => {
|
||||
p[v] = z.tuple([z.number(), z.number()]);
|
||||
return p;
|
||||
}, {} as Record<Ability, z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>)).partial(),
|
||||
spells: z.string().array(),
|
||||
modifiers: z.object(MAIN_STATS.reduce((p, v) => {
|
||||
p[v] = z.number();
|
||||
return p;
|
||||
}, {} as Record<MainStat, z.ZodNumber>)).partial(),
|
||||
owner: z.number(),
|
||||
username: z.string().optional(),
|
||||
visibility: z.enum(["public", "private"]),
|
||||
thumbnail: z.any(),
|
||||
});
|
||||
export type Character = {
|
||||
id: number;
|
||||
|
||||
name: string;
|
||||
people?: number;
|
||||
level: number;
|
||||
aspect?: number;
|
||||
notes?: string | null;
|
||||
health: number;
|
||||
mana: number;
|
||||
|
||||
training: Record<MainStat, DoubleIndex<TrainingLevel>[]>;
|
||||
leveling: DoubleIndex<Level>[];
|
||||
abilities: Partial<Record<Ability, [number, number]>>; //First is the ability, second is the max increment
|
||||
spells: string[]; //Spell ID
|
||||
modifiers: Partial<Record<MainStat, number>>;
|
||||
|
||||
owner: number;
|
||||
username?: string;
|
||||
visibility: "private" | "public";
|
||||
};
|
||||
export type CharacterValues = {
|
||||
health: number;
|
||||
mana: number;
|
||||
};
|
||||
export type CharacterConfig = {
|
||||
peoples: Race[],
|
||||
training: Record<MainStat, Record<TrainingLevel, TrainingOption[]>>;
|
||||
abilities: Record<Ability, AbilityConfig>;
|
||||
spells: SpellConfig[];
|
||||
};
|
||||
export type SpellConfig = {
|
||||
id: string;
|
||||
name: string;
|
||||
rank: 1 | 2 | 3 | 4;
|
||||
type: SpellType;
|
||||
cost: number;
|
||||
speed: "action" | "reaction" | number;
|
||||
elements: Array<SpellElement>;
|
||||
effect: string;
|
||||
tags?: string[];
|
||||
};
|
||||
export type AbilityConfig = {
|
||||
max: [MainStat, MainStat];
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
export type Race = {
|
||||
name: string;
|
||||
description: string;
|
||||
utils: {
|
||||
maxOption: number;
|
||||
};
|
||||
options: Record<Level, RaceOption[]>;
|
||||
};
|
||||
export type RaceOption = {
|
||||
training?: number;
|
||||
health?: number;
|
||||
mana?: number;
|
||||
shaping?: number;
|
||||
modifier?: number;
|
||||
abilities?: number;
|
||||
spellslots?: number;
|
||||
};
|
||||
export type FeatureItem = {
|
||||
category: "misc";
|
||||
text: string;
|
||||
} | {
|
||||
category: "action";
|
||||
cost: 1 | 2 | 3;
|
||||
text: string;
|
||||
} | {
|
||||
category: "reaction";
|
||||
cost: 1 | 2;
|
||||
text: string;
|
||||
} | {
|
||||
category: "freeaction";
|
||||
text: string;
|
||||
} | {
|
||||
category: "value";
|
||||
type: "add" | "remove" | "set";
|
||||
value: number | false;
|
||||
property: string;
|
||||
} | {
|
||||
category: "asset";
|
||||
type: "add" | "remove";
|
||||
kind: "spells";
|
||||
asset: string;
|
||||
}
|
||||
type FeatureCategory = FeatureItem["category"];
|
||||
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;
|
||||
race: number;
|
||||
spellslots: number;
|
||||
artslots: number;
|
||||
spellranks: Record<SpellType, 0 | 1 | 2 | 3>;
|
||||
aspect: string;
|
||||
speed: number | false;
|
||||
initiative: number;
|
||||
spells: string[];
|
||||
|
||||
values: CharacterValues,
|
||||
|
||||
defense: {
|
||||
hardcap: number;
|
||||
static: number;
|
||||
activeparry: number;
|
||||
activedodge: number;
|
||||
passiveparry: number;
|
||||
passivedodge: number;
|
||||
};
|
||||
|
||||
mastery: {
|
||||
strength: number;
|
||||
dexterity: number;
|
||||
shield: number;
|
||||
armor: number;
|
||||
multiattack: number;
|
||||
magicpower: number;
|
||||
magicspeed: number;
|
||||
magicelement: number;
|
||||
magicinstinct: number;
|
||||
};
|
||||
|
||||
bonus: Record<string, number>; //Any special bonus goes here
|
||||
resistance: Record<MainStat, number>;
|
||||
|
||||
modifier: Record<MainStat, number>;
|
||||
abilities: Partial<Record<Ability, number>>;
|
||||
level: number;
|
||||
features: { [K in FeatureCategory]: Array<Extract<FeatureItem, { category: K }>> };
|
||||
|
||||
notes: string;
|
||||
};
|
||||
Loading…
Reference in New Issue