story 3 was not completely done

This commit is contained in:
Willem van den Ende 2026-06-14 09:54:07 +01:00
parent f6605bbbfd
commit 350e8073e9
8 changed files with 39135 additions and 4054 deletions

View File

@ -1,20 +1,18 @@
/** /**
* CharacterState immutable record of all character state at a point in time. * CharacterState immutable record of all character state at a point in time.
* *
* Groups the five character properties into a single value object, * Groups the five character properties into a single value type,
* keeping the Character constructor at one parameter (max-params: 4). * keeping the Character constructor at one parameter.
*/ */
import type { Health } from './Health.ts'; import type { Health } from './Health.ts';
import type { Level } from './Level.ts'; import type { Level } from './Level.ts';
import type { Status } from './Status.ts'; import type { Status } from './Status.ts';
import type { Faction } from './Faction.ts'; import type { Faction } from './Faction.ts';
export class CharacterState { export type CharacterState = {
constructor( readonly name: string;
readonly name: string, readonly health: Health;
readonly health: Health, readonly status: Status;
readonly status: Status, readonly level: Level;
readonly level: Level, readonly factions: ReadonlySet<Faction>;
readonly factions: ReadonlySet<Faction>, };
) {}
}

View File

@ -1,24 +1,22 @@
/** /**
* Healing Object a Magical Object that gives health to Characters. * Healing Object a Magical Object that gives health to Characters.
* *
* Inherits health/status management from MagicalObject.
* Invariants enforced at construction: * Invariants enforced at construction:
* - Health is non-negative * - CurrentHealth never exceeds maxHealth
* - Health never exceeds maxHealth
*/ */
import { Character } from './Character.ts'; import { Character } from './Character.ts';
import { Level } from './Level.ts';
import { MagicalObject } from './MagicalObject.ts';
export type ObjectStatus = { kind: 'alive' } | { kind: 'destroyed' }; export class HealingObject extends MagicalObject {
private constructor(
export class HealingObject { health: number,
readonly #health: number; maxHealth: number,
readonly #maxHealth: number; status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
readonly #status: ObjectStatus; ) {
super(health, maxHealth, status);
private constructor(health: number, maxHealth: number, status: ObjectStatus) {
this.#health = health;
this.#maxHealth = maxHealth;
this.#status = status;
} }
static create({ static create({
@ -36,29 +34,17 @@ export class HealingObject {
return new HealingObject(currentHealth, maxHealth, status); return new HealingObject(currentHealth, maxHealth, status);
} }
get health(): number {
return this.#health;
}
get maxHealth(): number {
return this.#maxHealth;
}
get status(): ObjectStatus {
return this.#status;
}
/** Use this object to heal a character. Returns updated object and character. */ /** Use this object to heal a character. Returns updated object and character. */
heal(character: Character, amount: number): { object: HealingObject; character: Character } { heal(character: Character, amount: number): { object: HealingObject; character: Character } {
// Destroyed objects can't heal // Destroyed objects can't heal
if (this.#status.kind === 'destroyed') { if (this.status.kind === 'destroyed') {
return { object: this, character }; return { object: this, character };
} }
// Negative amount is invalid // Negative amount is invalid
if (amount < 0) throw new Error('Heal amount must be non-negative'); if (amount < 0) throw new Error('Heal amount must be non-negative');
// Calculate actual heal amount: min of requested, object remaining, character headroom // Calculate actual heal amount: min of requested, object remaining, character headroom
const objectRemaining = this.#health; const objectRemaining = this.health;
const characterMax = character.level.value >= 6 ? 1500 : 1000; const characterMax = Level.maxHealthForLevel(character.level.value);
const characterHeadroom = characterMax - character.health.value; const characterHeadroom = characterMax - character.health.value;
const actualHeal = Math.min(amount, objectRemaining, characterHeadroom); const actualHeal = Math.min(amount, objectRemaining, characterHeadroom);
// If actualHeal is 0, nothing changes // If actualHeal is 0, nothing changes
@ -66,7 +52,7 @@ export class HealingObject {
return { object: this, character }; return { object: this, character };
} }
// Create updated object // Create updated object
const newObjectHealth = this.#health - actualHeal; const newObjectHealth = this.health - actualHeal;
const newObjectStatus = const newObjectStatus =
newObjectHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const }; newObjectHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
// Create updated character // Create updated character
@ -77,7 +63,7 @@ export class HealingObject {
health: newCharacterHealth, health: newCharacterHealth,
}); });
return { return {
object: new HealingObject(newObjectHealth, this.#maxHealth, newObjectStatus), object: new HealingObject(newObjectHealth, this.maxHealth, newObjectStatus),
character: newCharacter, character: newCharacter,
}; };
} }

45
src/MagicalObject.ts Normal file
View File

@ -0,0 +1,45 @@
/**
* MagicalObject shared base for all magical items in the game.
*
* Invariants enforced at construction:
* - Health is non-negative
* - Health never exceeds maxHealth
* - Status derived from health (0 = destroyed, > 0 = alive)
*/
export type MagicalObjectStatus = { readonly kind: 'alive' } | { readonly kind: 'destroyed' };
export class MagicalObject {
readonly #health: number;
readonly #maxHealth: number;
readonly #status: MagicalObjectStatus;
protected constructor(health: number, maxHealth: number, status: MagicalObjectStatus) {
this.#health = health;
this.#maxHealth = maxHealth;
this.#status = status;
}
get health(): number {
return this.#health;
}
get maxHealth(): number {
return this.#maxHealth;
}
get status(): MagicalObjectStatus {
return this.#status;
}
/** Create a destroyed object (health = 0). */
static createDestroyed(maxHealth: number): MagicalObject {
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
return new MagicalObject(0, maxHealth, { kind: 'destroyed' });
}
/** Check if this object is alive. */
isAlive(): boolean {
return this.#status.kind === 'alive';
}
}

View File

@ -1,32 +1,25 @@
/** /**
* Magical Weapon a Magical Object that deals fixed damage. * Magical Weapon a Magical Object that deals fixed damage.
* *
* Inherits health/status management from MagicalObject.
* Invariants enforced at construction: * Invariants enforced at construction:
* - Health is non-negative
* - Health never exceeds maxHealth
* - Damage is non-negative * - Damage is non-negative
*/ */
import { Character } from './Character.ts'; import { Character } from './Character.ts';
import { MagicalObject } from './MagicalObject.ts';
export type WeaponStatus = { kind: 'alive' } | { kind: 'destroyed' }; export class MagicalWeapon extends MagicalObject {
export class MagicalWeapon {
readonly #health: number;
readonly #maxHealth: number;
readonly #status: WeaponStatus;
readonly #damage: number; readonly #damage: number;
readonly #owner: Character; readonly #owner: Character;
private constructor( private constructor(
health: number, health: number,
maxHealth: number, maxHealth: number,
status: WeaponStatus, status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
damage: number, damage: number,
owner: Character, owner: Character,
) { ) {
this.#health = health; super(health, maxHealth, status);
this.#maxHealth = maxHealth;
this.#status = status;
this.#damage = damage; this.#damage = damage;
this.#owner = owner; this.#owner = owner;
} }
@ -45,18 +38,6 @@ export class MagicalWeapon {
return new MagicalWeapon(maxHealth, maxHealth, { kind: 'alive' }, damage, owner); return new MagicalWeapon(maxHealth, maxHealth, { kind: 'alive' }, damage, owner);
} }
get health(): number {
return this.#health;
}
get maxHealth(): number {
return this.#maxHealth;
}
get status(): WeaponStatus {
return this.#status;
}
get damage(): number { get damage(): number {
return this.#damage; return this.#damage;
} }
@ -68,7 +49,7 @@ export class MagicalWeapon {
/** Use this weapon to deal damage. Returns updated weapon and target. */ /** Use this weapon to deal damage. Returns updated weapon and target. */
use(target: Character): { weapon: MagicalWeapon; target: Character } { use(target: Character): { weapon: MagicalWeapon; target: Character } {
// Destroyed weapons can't be used // Destroyed weapons can't be used
if (this.#status.kind === 'destroyed') { if (this.status.kind === 'destroyed') {
return { weapon: this, target }; return { weapon: this, target };
} }
// Deal fixed damage // Deal fixed damage
@ -81,18 +62,17 @@ export class MagicalWeapon {
status: newTargetStatus, status: newTargetStatus,
}); });
// Reduce weapon health by 1 // Reduce weapon health by 1
const newWeaponHealth = this.#health - 1; const newWeaponHealth = this.health - 1;
const newWeaponStatus = const newWeaponStatus =
newWeaponHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const }; newWeaponHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
return { return {
weapon: new MagicalWeapon( weapon: new MagicalWeapon(
newWeaponHealth, newWeaponHealth,
this.#maxHealth, this.maxHealth,
newWeaponStatus, newWeaponStatus,
this.#damage, this.#damage,
this.#owner, this.#owner,
), ),
target: newTarget, target: newTarget,
}; };
} }

View File

@ -7,11 +7,3 @@ export type Status = { readonly kind: 'alive' } | { readonly kind: 'dead' };
export const StatusAlive: Status = { kind: 'alive' }; export const StatusAlive: Status = { kind: 'alive' };
export const StatusDead: Status = { kind: 'dead' }; export const StatusDead: Status = { kind: 'dead' };
export function isAlive(s: Status): s is { kind: 'alive' } {
return s.kind === 'alive';
}
export function isDead(s: Status): s is { kind: 'dead' } {
return s.kind === 'dead';
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13112
transcripts/story-4-built.html Normal file

File diff suppressed because one or more lines are too long