obsidian-visualiser/shared/character.ts

273 lines
10 KiB
TypeScript

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;
}
}
}