- 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
82 lines
2.2 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|