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.
*
* Groups the five character properties into a single value object,
* keeping the Character constructor at one parameter (max-params: 4).
* Groups the five character properties into a single value type,
* keeping the Character constructor at one parameter.
*/
import type { Health } from './Health.ts';
import type { Level } from './Level.ts';
import type { Status } from './Status.ts';
import type { Faction } from './Faction.ts';
export class CharacterState {
constructor(
readonly name: string,
readonly health: Health,
readonly status: Status,
readonly level: Level,
readonly factions: ReadonlySet<Faction>,
) {}
}
export type CharacterState = {
readonly name: string;
readonly health: Health;
readonly status: Status;
readonly level: Level;
readonly factions: ReadonlySet<Faction>;
};

View File

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