rpg-combat-pi-01/src/Character.ts
Willem van den Ende fc2cf73b34 feat: story 2 — Characters can Deal Damage to Characters
- Allium spec: damage-and-health.allium
- 5 fast-check properties: damage reduces health, health non-negative,
  death at zero, self-damage forbidden, dead cannot take damage
- Character.createWithHealth factory for testing with arbitrary health
- Character.dealDamage(target, damage) method
2026-06-13 15:28:34 +01:00

82 lines
2.2 KiB
TypeScript

/**
* 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<Faction> {
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,
);
}
}