/** * Character entity — value-object-driven. * * "I can't believe it's not Haskell": invariants at boundaries. * State is encapsulated in a CharacterState record type. */ import { Health } from './Health.ts'; import type { Level } from './Level.ts'; import type { Status } from './Status.ts'; import { StatusAlive, StatusDead } from './Status.ts'; import { CharacterState } from './CharacterState.ts'; import type { Faction } from './Faction.ts'; export interface CharacterCtor { name: string; level: Level; } export interface CharacterCtorWithHealth { name: string; level: Level; health: number; } export class Character { #state: CharacterState; private constructor(state: CharacterState) { this.#state = state; } /** Create a new character with default health (1000) and alive status. */ static create({ name, level }: CharacterCtor): Character { const state = new CharacterState(name, Health.create(1000), StatusAlive, level, new Set()); return new Character(state); } /** Create a character with a specific health value (for testing). */ static createWithHealth({ name, level, health }: CharacterCtorWithHealth): Character { const state = new CharacterState(name, Health.create(health), StatusAlive, level, new Set()); return new Character(state); } get name(): string { return this.#state.name; } get health(): Health { return this.#state.health; } get status(): Status { return this.#state.status; } get level(): Level { return this.#state.level; } get factions(): ReadonlySet { return this.#state.factions; } /** Deal damage to another character. Mutates target in place. */ dealDamage(target: Character, damage: number): void { // Self-damage is forbidden if (this.name === target.name) return; // Dead characters cannot take damage if (target.status.kind === 'dead') return; // Reduce health const newHealth = target.health.sub(damage); const newStatus = newHealth.value === 0 ? StatusDead : StatusAlive; target.#state = new CharacterState( target.name, newHealth, newStatus, target.level, target.factions, ); } }