feat: restructure guardian progression system with dynamic element support
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s

This commit is contained in:
2026-05-29 17:18:13 +02:00
parent 644b76f16d
commit 71c68443c4
19 changed files with 757 additions and 446 deletions
+62 -63
View File
@@ -9,14 +9,14 @@ import {
getSpireRoomTypeDisplay,
SPIRE_CONFIG,
} from '../utils/spire-utils';
import { isGuardianFloor, getExtendedGuardian, getGuardianForFloor, getGuardianHP, generateGuardianName, generateComboGuardianName, ALL_GUARDIAN_FLOORS } from '../data/guardian-encounters';
import { isGuardianFloor, getGuardianForFloor, getGuardianHP, generateGuardianName, getAllGuardianFloors } from '../data/guardian-encounters';
// ─── Spire Utils ─────────────────────────────────────────────────────────────
describe('getRoomsForFloor', () => {
it('should return at least minRoomsPerFloor for non-guardian floors', () => {
for (let floor = 1; floor <= 50; floor++) {
if (floor % 10 === 0) continue; // Skip guardian floors
if (floor % 10 === 0) continue;
const rooms = getRoomsForFloor(floor);
expect(rooms).toBeGreaterThanOrEqual(SPIRE_CONFIG.minRoomsPerFloor);
expect(rooms).toBeLessThanOrEqual(SPIRE_CONFIG.maxRoomsPerFloor + 5);
@@ -46,16 +46,17 @@ describe('generateSpireRoomType', () => {
it('should return combat for first room on non-guardian floors', () => {
for (const floor of [1, 5, 15, 25]) {
const roomType = generateSpireRoomType(floor, 0, 10);
// First room may be combat, swarm, or speed depending on random
expect(['combat', 'swarm', 'speed']).toContain(roomType);
}
});
it('should return combat for first room on guardian floors (not last room)', () => {
// Floor 50 is a guardian floor, but first room should still be combat
const roomType = generateSpireRoomType(50, 0, 10);
// First room on guardian floor should not be 'guardian' (last room) and may be combat or swarm depending on random
expect(['combat', 'swarm']).toContain(roomType);
it('should return valid room type for first room on guardian floors (not last room)', () => {
// First room on non-last position should never be 'guardian'
for (let i = 0; i < 50; i++) {
const roomType = generateSpireRoomType(50, 0, 10);
expect(['combat', 'swarm', 'speed']).toContain(roomType);
expect(roomType).not.toBe('guardian');
}
});
it('should return valid room types', () => {
@@ -83,9 +84,7 @@ describe('generateSpireFloorState', () => {
});
it('should generate swarm floor with multiple enemies', () => {
// Force swarm by using a non-special room index
const state = generateSpireFloorState(20, 1, 10);
// Room type depends on random, but enemies should be valid
if (state.roomType === 'swarm') {
expect(state.enemies.length).toBeGreaterThanOrEqual(3);
}
@@ -122,7 +121,6 @@ describe('getSpireEnemyBarrier', () => {
for (let floor = 15; floor <= 100; floor++) {
const barrier = getSpireEnemyBarrier(floor, 'fire');
expect(barrier).toBeGreaterThanOrEqual(0);
// Use toBeLessThan with a small tolerance for floating point precision
expect(barrier).toBeLessThanOrEqual(0.3000000001);
}
});
@@ -182,53 +180,52 @@ describe('isGuardianFloor', () => {
});
});
describe('getExtendedGuardian (procedural combo guardians)', () => {
it('should return combo guardians for floors 150+', () => {
const g150 = getExtendedGuardian(150);
expect(g150).not.toBeNull();
expect(g150!.element).toContain('+');
});
it('should return null for floors below 150', () => {
expect(getExtendedGuardian(1)).toBeNull();
expect(getExtendedGuardian(15)).toBeNull();
expect(getExtendedGuardian(95)).toBeNull();
expect(getExtendedGuardian(100)).toBeNull();
expect(getExtendedGuardian(140)).toBeNull();
});
});
describe('getGuardianForFloor (unified lookup)', () => {
it('should return base element guardians for floors 10-70', () => {
expect(getGuardianForFloor(10)!.element).toBe('fire');
expect(getGuardianForFloor(20)!.element).toBe('water');
expect(getGuardianForFloor(30)!.element).toBe('air');
expect(getGuardianForFloor(40)!.element).toBe('earth');
expect(getGuardianForFloor(50)!.element).toBe('light');
expect(getGuardianForFloor(60)!.element).toBe('dark');
expect(getGuardianForFloor(70)!.element).toBe('death');
expect(getGuardianForFloor(10)!.element).toEqual(['fire']);
expect(getGuardianForFloor(20)!.element).toEqual(['water']);
expect(getGuardianForFloor(30)!.element).toEqual(['air']);
expect(getGuardianForFloor(40)!.element).toEqual(['earth']);
expect(getGuardianForFloor(50)!.element).toEqual(['light']);
expect(getGuardianForFloor(60)!.element).toEqual(['dark']);
expect(getGuardianForFloor(70)!.element).toEqual(['death']);
});
it('should return utility guardian for floor 80', () => {
expect(getGuardianForFloor(80)!.element).toBe('transference');
expect(getGuardianForFloor(80)!.element).toEqual(['transference']);
});
it('should return compound guardians for floors 90-110', () => {
expect(getGuardianForFloor(90)!.element).toBe('metal');
expect(getGuardianForFloor(100)!.element).toBe('sand');
expect(getGuardianForFloor(110)!.element).toBe('lightning');
it('should return composite guardians for floors 90-110', () => {
expect(getGuardianForFloor(90)!.element).toEqual(['metal']);
expect(getGuardianForFloor(100)!.element).toEqual(['sand']);
expect(getGuardianForFloor(110)!.element).toEqual(['lightning']);
});
it('should return exotic guardians for floors 120-140', () => {
expect(getGuardianForFloor(120)!.element).toBe('crystal');
expect(getGuardianForFloor(130)!.element).toBe('stellar');
expect(getGuardianForFloor(140)!.element).toBe('void');
it('should return multi-element guardians for tier 3 floors', () => {
expect(getGuardianForFloor(130)!.element).toEqual(['metal', 'fire', 'earth']);
expect(getGuardianForFloor(140)!.element).toEqual(['sand', 'earth', 'water']);
expect(getGuardianForFloor(150)!.element).toEqual(['lightning', 'fire', 'air']);
});
it('should return combo guardians for floors 150+', () => {
const g150 = getGuardianForFloor(150);
expect(g150).not.toBeNull();
expect(g150!.element).toContain('+');
it('should return exotic guardians for floors 170-200', () => {
expect(getGuardianForFloor(170)!.element).toEqual(['crystal']);
expect(getGuardianForFloor(180)!.element).toEqual(['stellar']);
expect(getGuardianForFloor(190)!.element).toEqual(['void']);
});
it('should return multi-element exotic convergence for floor 200', () => {
const g200 = getGuardianForFloor(200);
expect(g200).not.toBeNull();
expect(g200!.element.length).toBeGreaterThan(1);
expect(g200!.element).toContain('crystal');
expect(g200!.element).toContain('stellar');
expect(g200!.element).toContain('void');
});
it('should return guardians for procedural floors 210+', () => {
const g210 = getGuardianForFloor(210);
expect(g210).not.toBeNull();
expect(g210!.element.length).toBeGreaterThanOrEqual(2);
});
it('should return null for non-guardian floors', () => {
@@ -255,36 +252,38 @@ describe('getGuardianHP', () => {
describe('generateGuardianName', () => {
it('should generate non-empty names', () => {
for (const element of ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death']) {
const name = generateGuardianName(element);
const name = generateGuardianName([element]);
expect(name).toBeTruthy();
expect(name.length).toBeGreaterThan(0);
}
});
it('should include a title', () => {
const name = generateGuardianName('fire');
const name = generateGuardianName(['fire']);
expect(name).toContain(' the ');
});
});
describe('generateComboGuardianName', () => {
it('should combine two element prefixes', () => {
const name = generateComboGuardianName(['fire', 'water']);
expect(name).toContain(' the ');
expect(name.length).toBeGreaterThan(0);
});
});
describe('ALL_GUARDIAN_FLOORS', () => {
describe('getAllGuardianFloors', () => {
it('should include base guardian floors', () => {
expect(ALL_GUARDIAN_FLOORS).toContain(10);
expect(ALL_GUARDIAN_FLOORS).toContain(20);
expect(ALL_GUARDIAN_FLOORS).toContain(100);
const floors = getAllGuardianFloors();
expect(floors).toContain(10);
expect(floors).toContain(20);
expect(floors).toContain(100);
});
it('should be sorted', () => {
for (let i = 1; i < ALL_GUARDIAN_FLOORS.length; i++) {
expect(ALL_GUARDIAN_FLOORS[i]).toBeGreaterThan(ALL_GUARDIAN_FLOORS[i - 1]);
const floors = getAllGuardianFloors();
for (let i = 1; i < floors.length; i++) {
expect(floors[i]).toBeGreaterThan(floors[i - 1]);
}
});
it('should include procedural floors', () => {
const floors = getAllGuardianFloors();
expect(floors).toContain(210);
expect(floors).toContain(250);
expect(floors).toContain(300);
expect(floors).toContain(400);
});
});