feat: add upgrade scrolls
This commit is contained in:
64
src/engine/systems/UpgradeManager.ts
Normal file
64
src/engine/systems/UpgradeManager.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Item, WeaponItem, ArmourItem } from "../../core/types";
|
||||
|
||||
/**
|
||||
* Manages item upgrade logic for applying upgrade scrolls.
|
||||
*/
|
||||
export class UpgradeManager {
|
||||
|
||||
/**
|
||||
* Checks if an item can be upgraded (weapons and armour only).
|
||||
*/
|
||||
static canUpgrade(item: Item): boolean {
|
||||
return item.type === "Weapon" ||
|
||||
item.type === "BodyArmour" ||
|
||||
item.type === "Helmet" ||
|
||||
item.type === "Gloves" ||
|
||||
item.type === "Boots";
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies an upgrade to an item, increasing all stats by +1.
|
||||
* Returns true if successful.
|
||||
*/
|
||||
static applyUpgrade(item: Item): boolean {
|
||||
if (!this.canUpgrade(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increment upgrade level
|
||||
const currentLevel = item.upgradeLevel ?? 0;
|
||||
item.upgradeLevel = currentLevel + 1;
|
||||
|
||||
// Update item name with level suffix
|
||||
// Remove any existing upgrade suffix first
|
||||
const baseName = item.name.replace(/\s*\+\d+$/, "");
|
||||
item.name = `${baseName} +${item.upgradeLevel}`;
|
||||
|
||||
// Increase all numeric stats by +1
|
||||
if (item.type === "Weapon") {
|
||||
const weaponItem = item as WeaponItem;
|
||||
if (weaponItem.stats.attack !== undefined) {
|
||||
weaponItem.stats.attack += 1;
|
||||
}
|
||||
} else if (item.type === "BodyArmour" || item.type === "Helmet" ||
|
||||
item.type === "Gloves" || item.type === "Boots") {
|
||||
const armourItem = item as ArmourItem;
|
||||
if (armourItem.stats.defense !== undefined) {
|
||||
armourItem.stats.defense += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name for an item including upgrade level.
|
||||
*/
|
||||
static getDisplayName(item: Item): string {
|
||||
if (item.upgradeLevel && item.upgradeLevel > 0) {
|
||||
const baseName = item.name.replace(/\s*\+\d+$/, "");
|
||||
return `${baseName} +${item.upgradeLevel}`;
|
||||
}
|
||||
return item.name;
|
||||
}
|
||||
}
|
||||
66
src/engine/systems/__tests__/UpgradeManager.test.ts
Normal file
66
src/engine/systems/__tests__/UpgradeManager.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UpgradeManager } from '../UpgradeManager';
|
||||
import { createMeleeWeapon, createArmour, createConsumable } from '../../../core/config/Items';
|
||||
import type { WeaponItem, ArmourItem } from '../../../core/types';
|
||||
|
||||
describe('UpgradeManager', () => {
|
||||
it('should correctly identify upgradeable items', () => {
|
||||
const sword = createMeleeWeapon("iron_sword");
|
||||
const armor = createArmour("leather_armor");
|
||||
const potion = createConsumable("health_potion");
|
||||
|
||||
expect(UpgradeManager.canUpgrade(sword)).toBe(true);
|
||||
expect(UpgradeManager.canUpgrade(armor)).toBe(true);
|
||||
expect(UpgradeManager.canUpgrade(potion)).toBe(false);
|
||||
});
|
||||
|
||||
it('should upgrade weapon stats and name', () => {
|
||||
const sword = createMeleeWeapon("iron_sword") as WeaponItem;
|
||||
const initialAttack = sword.stats.attack!;
|
||||
const initialName = sword.name;
|
||||
|
||||
const success = UpgradeManager.applyUpgrade(sword);
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(sword.stats.attack).toBe(initialAttack + 1);
|
||||
expect(sword.upgradeLevel).toBe(1);
|
||||
expect(sword.name).toBe(`${initialName} +1`);
|
||||
});
|
||||
|
||||
it('should upgrade armour stats and name', () => {
|
||||
const armor = createArmour("leather_armor") as ArmourItem;
|
||||
const initialDefense = armor.stats.defense!;
|
||||
const initialName = armor.name;
|
||||
|
||||
const success = UpgradeManager.applyUpgrade(armor);
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(armor.stats.defense).toBe(initialDefense + 1);
|
||||
expect(armor.upgradeLevel).toBe(1);
|
||||
expect(armor.name).toBe(`${initialName} +1`);
|
||||
});
|
||||
|
||||
it('should handle sequential upgrades', () => {
|
||||
const sword = createMeleeWeapon("iron_sword") as WeaponItem;
|
||||
const initialAttack = sword.stats.attack!;
|
||||
const initialName = sword.name;
|
||||
|
||||
UpgradeManager.applyUpgrade(sword); // +1
|
||||
UpgradeManager.applyUpgrade(sword); // +2
|
||||
|
||||
expect(sword.stats.attack).toBe(initialAttack + 2);
|
||||
expect(sword.upgradeLevel).toBe(2);
|
||||
expect(sword.name).toBe(`${initialName} +2`);
|
||||
});
|
||||
|
||||
it('should not upgrade non-upgradeable items', () => {
|
||||
const potion = createConsumable("health_potion");
|
||||
const initialName = potion.name;
|
||||
|
||||
const success = UpgradeManager.applyUpgrade(potion);
|
||||
|
||||
expect(success).toBe(false);
|
||||
expect(potion.upgradeLevel).toBeUndefined();
|
||||
expect(potion.name).toBe(initialName);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
createConsumable,
|
||||
createMeleeWeapon,
|
||||
createRangedWeapon,
|
||||
createArmour
|
||||
createArmour,
|
||||
createUpgradeScroll
|
||||
} from "../../core/config/Items";
|
||||
import { seededRandom } from "../../core/math";
|
||||
import * as ROT from "rot-js";
|
||||
@@ -62,7 +63,8 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
createMeleeWeapon("iron_sword", "sharp"), // Sharp sword variant
|
||||
createConsumable("throwing_dagger", 3),
|
||||
createRangedWeapon("pistol"),
|
||||
createArmour("leather_armor", "heavy") // Heavy armour variant
|
||||
createArmour("leather_armor", "heavy"), // Heavy armour variant
|
||||
createUpgradeScroll(2) // 2 Upgrade scrolls
|
||||
] : [])
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user