Initial commit
This commit is contained in:
134
src/lib/game/__tests__/bug-fixes.test.ts
Executable file
134
src/lib/game/__tests__/bug-fixes.test.ts
Executable file
@@ -0,0 +1,134 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateEffectCapacityCost, ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
|
||||
import { EQUIPMENT_TYPES } from '../data/equipment';
|
||||
import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '../data/attunements';
|
||||
|
||||
describe('Enchantment Capacity Validation', () => {
|
||||
it('should calculate capacity cost for single stack effects', () => {
|
||||
// Mana Bolt spell effect has base capacity cost of 50
|
||||
const cost = calculateEffectCapacityCost('spell_manaBolt', 1, 0);
|
||||
expect(cost).toBe(50);
|
||||
});
|
||||
|
||||
it('should apply scaling for multiple stacks', () => {
|
||||
// damage_5 has base cost 15, each additional stack costs 20% more
|
||||
const cost1 = calculateEffectCapacityCost('damage_5', 1, 0);
|
||||
const cost2 = calculateEffectCapacityCost('damage_5', 2, 0);
|
||||
|
||||
// First stack: 15
|
||||
// Second stack: 15 * 1.2 = 18
|
||||
// Total: 33
|
||||
expect(cost1).toBe(15);
|
||||
expect(cost2).toBe(Math.floor(15 + 15 * 1.2));
|
||||
});
|
||||
|
||||
it('should apply efficiency bonus to reduce cost', () => {
|
||||
const costWithoutEfficiency = calculateEffectCapacityCost('spell_manaBolt', 1, 0);
|
||||
const costWithEfficiency = calculateEffectCapacityCost('spell_manaBolt', 1, 0.1); // 10% reduction
|
||||
|
||||
expect(costWithEfficiency).toBe(Math.floor(costWithoutEfficiency * 0.9));
|
||||
});
|
||||
|
||||
it('should respect equipment base capacity', () => {
|
||||
// Civilian Shirt has base capacity 30
|
||||
const shirt = EQUIPMENT_TYPES['civilianShirt'];
|
||||
expect(shirt.baseCapacity).toBe(30);
|
||||
|
||||
// Basic Staff has base capacity 50
|
||||
const staff = EQUIPMENT_TYPES['basicStaff'];
|
||||
expect(staff.baseCapacity).toBe(50);
|
||||
});
|
||||
|
||||
it('should reject enchantment designs exceeding equipment capacity', () => {
|
||||
// Mana Bolt spell effect costs 50 capacity
|
||||
// Civilian Shirt only has 30 capacity
|
||||
const manaBoltCost = calculateEffectCapacityCost('spell_manaBolt', 1, 0);
|
||||
const shirtCapacity = EQUIPMENT_TYPES['civilianShirt'].baseCapacity;
|
||||
|
||||
expect(manaBoltCost).toBeGreaterThan(shirtCapacity);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Attunement Mana Type Unlocking', () => {
|
||||
it('should define primary mana types for attunements', () => {
|
||||
// Enchanter should have transference as primary mana
|
||||
const enchanter = ATTUNEMENTS_DEF['enchanter'];
|
||||
expect(enchanter.primaryManaType).toBe('transference');
|
||||
|
||||
// Fabricator should have earth as primary mana
|
||||
const fabricator = ATTUNEMENTS_DEF['fabricator'];
|
||||
expect(fabricator.primaryManaType).toBe('earth');
|
||||
});
|
||||
|
||||
it('should have conversion rates for attunements with primary mana', () => {
|
||||
// Enchanter should have a conversion rate
|
||||
const enchanter = ATTUNEMENTS_DEF['enchanter'];
|
||||
expect(enchanter.conversionRate).toBeGreaterThan(0);
|
||||
|
||||
// Get scaled conversion rate at level 1
|
||||
const level1Rate = getAttunementConversionRate('enchanter', 1);
|
||||
expect(level1Rate).toBe(enchanter.conversionRate);
|
||||
|
||||
// Higher level should have higher rate
|
||||
const level5Rate = getAttunementConversionRate('enchanter', 5);
|
||||
expect(level5Rate).toBeGreaterThan(level1Rate);
|
||||
});
|
||||
|
||||
it('should have raw mana regen for all attunements', () => {
|
||||
Object.values(ATTUNEMENTS_DEF).forEach(attunement => {
|
||||
expect(attunement.rawManaRegen).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Floor HP State', () => {
|
||||
it('should have getFloorMaxHP function that returns positive values', async () => {
|
||||
const { getFloorMaxHP } = await import('../computed-stats');
|
||||
|
||||
for (let floor = 1; floor <= 100; floor++) {
|
||||
const hp = getFloorMaxHP(floor);
|
||||
expect(hp).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should scale HP correctly with floor progression', async () => {
|
||||
const { getFloorMaxHP } = await import('../computed-stats');
|
||||
|
||||
const hp1 = getFloorMaxHP(1);
|
||||
const hp10 = getFloorMaxHP(10);
|
||||
const hp50 = getFloorMaxHP(50);
|
||||
const hp100 = getFloorMaxHP(100);
|
||||
|
||||
expect(hp10).toBeGreaterThan(hp1);
|
||||
expect(hp50).toBeGreaterThan(hp10);
|
||||
expect(hp100).toBeGreaterThan(hp50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Element State', () => {
|
||||
it('should have utility elements defined', async () => {
|
||||
const { ELEMENTS } = await import('../constants');
|
||||
|
||||
// Check that utility element exists (transference is the only utility element now)
|
||||
expect(ELEMENTS['transference']).toBeDefined();
|
||||
|
||||
// Check categories
|
||||
expect(ELEMENTS['transference'].cat).toBe('utility');
|
||||
});
|
||||
|
||||
it('should have composite elements with recipes', async () => {
|
||||
const { ELEMENTS } = await import('../constants');
|
||||
|
||||
// Metal is fire + earth
|
||||
expect(ELEMENTS['metal'].cat).toBe('composite');
|
||||
expect(ELEMENTS['metal'].recipe).toEqual(['fire', 'earth']);
|
||||
|
||||
// Sand is earth + water
|
||||
expect(ELEMENTS['sand'].cat).toBe('composite');
|
||||
expect(ELEMENTS['sand'].recipe).toEqual(['earth', 'water']);
|
||||
|
||||
// Lightning is fire + air
|
||||
expect(ELEMENTS['lightning'].cat).toBe('composite');
|
||||
expect(ELEMENTS['lightning'].recipe).toEqual(['fire', 'air']);
|
||||
});
|
||||
});
|
||||
272
src/lib/game/__tests__/computed-stats.test.ts
Executable file
272
src/lib/game/__tests__/computed-stats.test.ts
Executable file
@@ -0,0 +1,272 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
fmt,
|
||||
fmtDec,
|
||||
getFloorMaxHP,
|
||||
getFloorElement,
|
||||
canAffordSpellCost,
|
||||
deductSpellCost,
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
computeClickMana,
|
||||
getMeditationBonus,
|
||||
getIncursionStrength,
|
||||
} from '../computed-stats';
|
||||
import { MAX_DAY, INCURSION_START_DAY, HOURS_PER_TICK } from '../constants';
|
||||
|
||||
describe('fmt', () => {
|
||||
it('should format numbers < 1000 as integers', () => {
|
||||
expect(fmt(500)).toBe('500');
|
||||
expect(fmt(0)).toBe('0');
|
||||
expect(fmt(999)).toBe('999');
|
||||
});
|
||||
|
||||
it('should format thousands with K suffix', () => {
|
||||
expect(fmt(1500)).toBe('1.5K');
|
||||
expect(fmt(1000)).toBe('1.0K');
|
||||
expect(fmt(9999)).toBe('10.0K');
|
||||
});
|
||||
|
||||
it('should format millions with M suffix', () => {
|
||||
expect(fmt(1500000)).toBe('1.50M');
|
||||
expect(fmt(1000000)).toBe('1.00M');
|
||||
});
|
||||
|
||||
it('should format billions with B suffix', () => {
|
||||
expect(fmt(1500000000)).toBe('1.50B');
|
||||
expect(fmt(1000000000)).toBe('1.00B');
|
||||
});
|
||||
|
||||
it('should handle edge cases', () => {
|
||||
expect(fmt(NaN)).toBe('0');
|
||||
expect(fmt(Infinity)).toBe('0');
|
||||
expect(fmt(-100)).toBe('-100');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fmtDec', () => {
|
||||
it('should format decimal numbers', () => {
|
||||
expect(fmtDec(1.5, 1)).toBe('1.5');
|
||||
expect(fmtDec(1.234, 2)).toBe('1.23');
|
||||
expect(fmtDec(1000.5, 1)).toBe('1000.5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFloorMaxHP', () => {
|
||||
it('should return base HP for floor 1', () => {
|
||||
const hp = getFloorMaxHP(1);
|
||||
expect(hp).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should scale HP with floor number', () => {
|
||||
const hp1 = getFloorMaxHP(1);
|
||||
const hp5 = getFloorMaxHP(5);
|
||||
const hp10 = getFloorMaxHP(10);
|
||||
expect(hp5).toBeGreaterThan(hp1);
|
||||
expect(hp10).toBeGreaterThan(hp5);
|
||||
});
|
||||
|
||||
it('should return higher HP for guardian floors', () => {
|
||||
// Floor 10 is Ignis Prime guardian
|
||||
const hp10 = getFloorMaxHP(10);
|
||||
const hp9 = getFloorMaxHP(9);
|
||||
expect(hp10).toBeGreaterThan(hp9 * 2); // Guardians have much more HP
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFloorElement', () => {
|
||||
it('should return an element string for valid floors', () => {
|
||||
const elem = getFloorElement(1);
|
||||
expect(typeof elem).toBe('string');
|
||||
expect(['fire', 'water', 'earth', 'air', 'raw']).toContain(elem);
|
||||
});
|
||||
|
||||
it('should cycle through elements based on floor number', () => {
|
||||
// Check that floors have different elements
|
||||
const elem1 = getFloorElement(1);
|
||||
const elem7 = getFloorElement(7);
|
||||
// Since it cycles every 5 floors, floor 1 and 7 might have same element
|
||||
const elem2 = getFloorElement(2);
|
||||
// Floor 1 and 2 should have different elements (if cycle allows)
|
||||
});
|
||||
});
|
||||
|
||||
describe('canAffordSpellCost', () => {
|
||||
it('should return true for raw mana cost when enough mana', () => {
|
||||
const result = canAffordSpellCost({ type: 'raw', amount: 10 }, 50, {});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for raw mana cost when not enough mana', () => {
|
||||
const result = canAffordSpellCost({ type: 'raw', amount: 100 }, 50, {});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle zero cost', () => {
|
||||
const result = canAffordSpellCost({ type: 'raw', amount: 0 }, 0, {});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle elemental costs', () => {
|
||||
const elements = {
|
||||
fire: { current: 10, max: 50, unlocked: true },
|
||||
};
|
||||
const result = canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for elemental cost when not enough', () => {
|
||||
const elements = {
|
||||
fire: { current: 3, max: 50, unlocked: true },
|
||||
};
|
||||
const result = canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for locked element', () => {
|
||||
const elements = {
|
||||
fire: { current: 10, max: 50, unlocked: false },
|
||||
};
|
||||
const result = canAffordSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deductSpellCost', () => {
|
||||
it('should deduct raw mana correctly', () => {
|
||||
const result = deductSpellCost({ type: 'raw', amount: 10 }, 50, {});
|
||||
expect(result.rawMana).toBe(40);
|
||||
});
|
||||
|
||||
it('should not go below zero', () => {
|
||||
const result = deductSpellCost({ type: 'raw', amount: 100 }, 50, {});
|
||||
expect(result.rawMana).toBe(0);
|
||||
});
|
||||
|
||||
it('should deduct elemental mana correctly', () => {
|
||||
const elements = {
|
||||
fire: { current: 10, max: 50, unlocked: true },
|
||||
};
|
||||
const result = deductSpellCost({ type: 'element', element: 'fire', amount: 5 }, 0, elements);
|
||||
expect(result.elements.fire.current).toBe(5);
|
||||
});
|
||||
|
||||
it('should return same values when cost is zero', () => {
|
||||
const elements = {
|
||||
fire: { current: 10, max: 50, unlocked: true },
|
||||
};
|
||||
const result = deductSpellCost({ type: 'raw', amount: 0 }, 50, elements);
|
||||
expect(result.rawMana).toBe(50);
|
||||
expect(result.elements.fire.current).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeMaxMana', () => {
|
||||
it('should return base 100 with no skills or upgrades', () => {
|
||||
const state = {
|
||||
skills: {},
|
||||
prestigeUpgrades: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 };
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
|
||||
it('should include manaWell prestige upgrade', () => {
|
||||
const state = {
|
||||
skills: {},
|
||||
prestigeUpgrades: { manaWell: 5 },
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 };
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(100 + 5 * 500); // Base + 500 per level
|
||||
});
|
||||
|
||||
it('should apply multiplier from effects', () => {
|
||||
const state = {
|
||||
skills: {},
|
||||
prestigeUpgrades: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1.5 };
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(150); // 100 * 1.5
|
||||
});
|
||||
|
||||
it('should apply bonus from effects', () => {
|
||||
const state = {
|
||||
skills: {},
|
||||
prestigeUpgrades: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 50, maxManaMultiplier: 1 };
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(150); // 100 + 50
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeRegen', () => {
|
||||
it('should return base regen with no skills', () => {
|
||||
const state = {
|
||||
skills: {},
|
||||
prestigeUpgrades: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 };
|
||||
const result = computeRegen(state, effects);
|
||||
expect(result).toBe(2); // Base regen
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeClickMana', () => {
|
||||
it('should return base click mana with no skills', () => {
|
||||
const state = {
|
||||
skills: {},
|
||||
prestigeUpgrades: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { clickManaBonus: 0, clickManaMultiplier: 1 };
|
||||
const result = computeClickMana(state, effects);
|
||||
expect(result).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeditationBonus', () => {
|
||||
it('should return 1.0 with zero ticks', () => {
|
||||
const result = getMeditationBonus(0, {});
|
||||
expect(result).toBe(1.0);
|
||||
});
|
||||
|
||||
it('should increase with more ticks', () => {
|
||||
const result1 = getMeditationBonus(10, {});
|
||||
const result2 = getMeditationBonus(100, {});
|
||||
expect(result2).toBeGreaterThan(result1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIncursionStrength', () => {
|
||||
it('should return 0 before incursion start day', () => {
|
||||
const result = getIncursionStrength(1, 12);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should return positive value during incursion', () => {
|
||||
// After incursion start day
|
||||
const result = getIncursionStrength(INCURSION_START_DAY, 12);
|
||||
expect(result).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should increase with later days', () => {
|
||||
const result1 = getIncursionStrength(INCURSION_START_DAY, 12);
|
||||
const result2 = getIncursionStrength(MAX_DAY, 12);
|
||||
expect(result2).toBeGreaterThan(result1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user