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[]>), leveling: [[1, 0]], abilities: {}, spells: [], modifiers: {}, choices: {}, owner: -1, visibility: "private", }; export const mainStatTexts: Record = { "strength": "Force", "dexterity": "Dextérité", "constitution": "Constitution", "intelligence": "Intelligence", "curiosity": "Curiosité", "charisma": "Charisme", "psyche": "Psyché", }; export const elementTexts: Record = { 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 = { "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>>)), 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>)).partial(), spells: z.string().array(), modifiers: z.object(MAIN_STATS.reduce((p, v) => { p[v] = z.number(); return p; }, {} as Record)).partial(), owner: z.number(), username: z.string().optional(), visibility: z.enum(["public", "private"]), thumbnail: z.any(), }); type PropertySum = { list: Array, value: number, _dirty: boolean }; export class CharacterBuilder { private _character: Character; private _result!: CompiledCharacter; private _buffer: Record = {}; 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 { const keys = Object.keys(this._buffer); this.compile(keys); return keys.reduce((p, v) => { p[v] = this._buffer[v].value; return p; }, {} as Record); } 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; } } }