refactor: replace number health with Health value object in MagicalObject

- MagicalObject: internal #health and #maxHealth now Health type
- HealingObject: constructor, create, and heal use Health value object
- MagicalWeapon: constructor, create, and use use Health value object
- DamageDealer and Healer interfaces: health: number -> health: Health
- magical-objects.spec.ts: all assertions use .health.value
- Run npm run checks: 0 errors, 70 tests passing
This commit is contained in:
2026-06-14 13:35:44 +01:00
parent 0540e5ff5b
commit ba0903714c
13 changed files with 51679 additions and 11982 deletions
+17 -14
View File
@@ -38,7 +38,7 @@ describe('Magical Objects', () => {
const target = Character.create({ name: 'goblin', level: Level.create(1) });
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
const result = attacker.useWeapon(weapon, target);
return result.weapon.health === weaponHP - 1;
return result.weapon.health.value === weaponHP - 1;
},
),
);
@@ -86,11 +86,11 @@ describe('Magical Objects', () => {
// Kill the attacker using a separate killer
const killer = Character.create({ name: 'boss', level: Level.create(1) });
const deadAttacker = killer.dealDamage(attacker, 10000);
const weaponHPBefore = weapon.health;
const weaponHPBefore = weapon.health.value;
const targetHealthBefore = target.health.value;
const result = deadAttacker.useWeapon(weapon, target);
return (
result.weapon.health === weaponHPBefore &&
result.weapon.health.value === weaponHPBefore &&
result.target.health.value === targetHealthBefore
);
},
@@ -110,11 +110,11 @@ describe('Magical Objects', () => {
const thief = Character.create({ name: 'thief', level: Level.create(1) });
const target = Character.create({ name: 'goblin', level: Level.create(1) });
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner });
const weaponHPBefore = weapon.health;
const weaponHPBefore = weapon.health.value;
const targetHealthBefore = target.health.value;
const result = thief.useWeapon(weapon, target);
return (
result.weapon.health === weaponHPBefore &&
result.weapon.health.value === weaponHPBefore &&
result.target.health.value === targetHealthBefore
);
},
@@ -136,7 +136,9 @@ describe('Magical Objects', () => {
const targetHealthBefore = firstUse.target.health.value;
// Try to use again on the destroyed weapon
const result = owner.useWeapon(destroyedWeapon, firstUse.target);
return result.weapon.health === 0 && result.target.health.value === targetHealthBefore;
return (
result.weapon.health.value === 0 && result.target.health.value === targetHealthBefore
);
}),
);
});
@@ -203,7 +205,7 @@ describe('Magical Objects', () => {
});
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
const result = character.useHealingObject(object, healAmount);
return result.object.health === objectHP - healAmount;
return result.object.health.value === objectHP - healAmount;
},
),
);
@@ -255,11 +257,11 @@ describe('Magical Objects', () => {
// Kill the character using a separate killer
const killer = Character.create({ name: 'boss', level: Level.create(1) });
const deadCharacter = killer.dealDamage(character, 10000);
const objectHPBefore = object.health;
const objectHPBefore = object.health.value;
const characterHealthBefore = deadCharacter.health.value;
const result = deadCharacter.useHealingObject(object, 100);
return (
result.object.health === objectHPBefore &&
result.object.health.value === objectHPBefore &&
result.character.health.value === characterHealthBefore
);
}),
@@ -287,7 +289,8 @@ describe('Magical Objects', () => {
// Try to use again on the destroyed object
const result = healedCharacter.useHealingObject(destroyedObject, 100);
return (
result.object.health === 0 && result.character.health.value === characterHealthBefore
result.object.health.value === 0 &&
result.character.health.value === characterHealthBefore
);
}),
);
@@ -305,7 +308,7 @@ describe('Magical Objects', () => {
const target = Character.create({ name: 'goblin', level: Level.create(1) });
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
const result = attacker.useWeapon(weapon, target);
return result.weapon.health >= 0;
return result.weapon.health.value >= 0;
},
),
);
@@ -320,7 +323,7 @@ describe('Magical Objects', () => {
const character = Character.create({ name: 'hero', level: Level.create(1) });
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
const result = character.useHealingObject(object, healAmount);
return result.object.health >= 0;
return result.object.health.value >= 0;
},
),
);
@@ -336,7 +339,7 @@ describe('Magical Objects', () => {
const target = Character.create({ name: 'goblin', level: Level.create(1) });
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
const result = attacker.useWeapon(weapon, target);
return result.weapon.health <= weaponHP;
return result.weapon.health.value <= weaponHP;
},
),
);
@@ -351,7 +354,7 @@ describe('Magical Objects', () => {
const character = Character.create({ name: 'hero', level: Level.create(1) });
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
const result = character.useHealingObject(object, healAmount);
return result.object.health <= objectHP;
return result.object.health.value <= objectHP;
},
),
);
+7 -6
View File
@@ -7,14 +7,15 @@
*/
import { Character } from '../characters/Character.ts';
import { Health } from '../value-objects/Health.ts';
import { Level } from '../value-objects/Level.ts';
import { MagicalObject } from './MagicalObject.ts';
import type { Healer } from './magical-object-types.ts';
export class HealingObject extends MagicalObject implements Healer {
private constructor(
health: number,
maxHealth: number,
health: Health,
maxHealth: Health,
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
) {
super(health, maxHealth, status);
@@ -32,7 +33,7 @@ export class HealingObject extends MagicalObject implements Healer {
if (currentHealth > maxHealth) throw new Error('CurrentHealth cannot exceed maxHealth');
const status =
currentHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
return new HealingObject(currentHealth, maxHealth, status);
return new HealingObject(Health.create(currentHealth), Health.create(maxHealth), status);
}
/** Use this object to heal a character. Returns updated object and character. */
@@ -44,7 +45,7 @@ export class HealingObject extends MagicalObject implements Healer {
// 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 objectRemaining = this.health.value;
const characterMax = Level.maxHealthForLevel(character.level.value);
const characterHeadroom = characterMax - character.health.value;
const actualHeal = Math.min(amount, objectRemaining, characterHeadroom);
@@ -53,7 +54,7 @@ export class HealingObject extends MagicalObject implements Healer {
return { object: this, character };
}
// Create updated object
const newObjectHealth = this.health - actualHeal;
const newObjectHealth = this.health.value - actualHeal;
const newObjectStatus =
newObjectHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
// Create updated character
@@ -64,7 +65,7 @@ export class HealingObject extends MagicalObject implements Healer {
health: newCharacterHealth,
});
return {
object: new HealingObject(newObjectHealth, this.maxHealth, newObjectStatus),
object: new HealingObject(Health.create(newObjectHealth), this.maxHealth, newObjectStatus),
character: newCharacter,
};
}
+8 -6
View File
@@ -7,24 +7,26 @@
* - Status derived from health (0 = destroyed, > 0 = alive)
*/
import { Health } from '../value-objects/Health.ts';
export type MagicalObjectStatus = { readonly kind: 'alive' } | { readonly kind: 'destroyed' };
export class MagicalObject {
readonly #health: number;
readonly #maxHealth: number;
readonly #health: Health;
readonly #maxHealth: Health;
readonly #status: MagicalObjectStatus;
protected constructor(health: number, maxHealth: number, status: MagicalObjectStatus) {
protected constructor(health: Health, maxHealth: Health, status: MagicalObjectStatus) {
this.#health = health;
this.#maxHealth = maxHealth;
this.#status = status;
}
get health(): number {
get health(): Health {
return this.#health;
}
get maxHealth(): number {
get maxHealth(): Health {
return this.#maxHealth;
}
@@ -35,7 +37,7 @@ export class MagicalObject {
/** 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' });
return new MagicalObject(Health.create(0), Health.create(maxHealth), { kind: 'destroyed' });
}
/** Check if this object is alive. */
+12 -5
View File
@@ -6,6 +6,7 @@
* - Damage is non-negative
*/
import { Character } from '../characters/Character.ts';
import { Health } from '../value-objects/Health.ts';
import { MagicalObject } from './MagicalObject.ts';
import type { DamageDealer } from './magical-object-types.ts';
@@ -14,8 +15,8 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
readonly #owner: Character;
private constructor(
health: number,
maxHealth: number,
health: Health,
maxHealth: Health,
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
damage: number,
owner: Character,
@@ -36,7 +37,13 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
}): MagicalWeapon {
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
if (damage < 0) throw new Error('Damage cannot be negative');
return new MagicalWeapon(maxHealth, maxHealth, { kind: 'alive' }, damage, owner);
return new MagicalWeapon(
Health.create(maxHealth),
Health.create(maxHealth),
{ kind: 'alive' },
damage,
owner,
);
}
get damage(): number {
@@ -63,12 +70,12 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
status: newTargetStatus,
});
// Reduce weapon health by 1
const newWeaponHealth = this.health - 1;
const newWeaponHealth = this.health.value - 1;
const newWeaponStatus =
newWeaponHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
return {
weapon: new MagicalWeapon(
newWeaponHealth,
Health.create(newWeaponHealth),
this.maxHealth,
newWeaponStatus,
this.#damage,
+3 -2
View File
@@ -6,19 +6,20 @@
* so Character can depend on abstractions rather than concrete classes.
*/
import type { Character } from '../characters/Character.ts';
import type { Health } from '../value-objects/Health.ts';
import type { MagicalObjectStatus } from './MagicalObject.ts';
/** A magical object that deals damage — from Character's point of view */
export interface DamageDealer {
readonly owner: Character;
readonly health: number;
readonly health: Health;
readonly status: MagicalObjectStatus;
use(target: Character): { weapon: DamageDealer; target: Character };
}
/** A magical object that heals — from Character's point of view */
export interface Healer {
readonly health: number;
readonly health: Health;
readonly status: MagicalObjectStatus;
heal(character: Character, amount: number): { object: Healer; character: Character };
}