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:
+17
-14
@@ -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,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user