Files
Emberfall/tests/unit/rewards-leveling.test.js
T
bepis f1ae7b3119 feat: complete Phase 4 combat/hunting system
- Add content migration (003_content.sql): zones, monsters, items, loot tables
- Add inventory migration (004_inventory.sql) and logs migration (006_logs.sql)
- Add Mulberry32 PRNG (src/utils/rng.js) for deterministic combat
- Add combat system (src/game/combat.js) with turn order, crit, mage bonus, 20-round cap
- Add rewards/loot system (src/game/rewards.js) and leveling (src/game/leveling.js)
- Add hunt endpoint (POST /api/hunt) with full combat flow in DB transaction
- Add hunt integration tests and unit tests for rewards/leveling/combat
- Fix session middleware to use memory store in tests (avoids cleanup hang)
- Fix huntService transaction (db.transaction() returns wrapper, must call separately)
- Add huntService to helpers.js cache invalidation for test isolation
2026-10-20 17:46:18 +11:00

157 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Tests for src/game/rewards.js and src/game/leveling.js.
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { calculateRewards, rollLoot, materialSellPrice } = require('../../src/game/rewards');
const { xpToNext, checkLevelUp, allocatePoints } = require('../../src/game/leveling');
const { mulberry32 } = require('../../src/utils/rng');
describe('rewards', () => {
it('calculateRewards — equal level gives base rewards', () => {
const rng = mulberry32(1);
const result = calculateRewards(100, 10, 20, 5, 5, rng);
assert.ok(result.xp > 0);
assert.ok(result.gold >= 10);
assert.ok(result.gold <= 20);
});
it('calculateRewards — higher level monster gives bonus', () => {
const rng = mulberry32(1);
const result = calculateRewards(100, 10, 20, 10, 5, rng);
// levelDiffMult = clamp(1 + 0.1*(10-5), 0.2, 1.5) = clamp(1.5, 0.2, 1.5) = 1.5
assert.strictEqual(result.xp, Math.floor(100 * 1.5));
});
it('calculateRewards — lower level monster gives reduced rewards', () => {
const rng = mulberry32(1);
const result = calculateRewards(100, 10, 20, 2, 5, rng);
// levelDiffMult = clamp(1 + 0.1*(2-5), 0.2, 1.5) = clamp(0.7, 0.2, 1.5) = 0.7
assert.strictEqual(result.xp, Math.floor(100 * 0.7));
});
it('calculateRewards — max 1.5x cap', () => {
const rng = mulberry32(1);
const result = calculateRewards(100, 10, 20, 20, 5, rng);
// levelDiffMult = clamp(1 + 0.1*(20-5), 0.2, 1.5) = clamp(2.5, 0.2, 1.5) = 1.5
assert.strictEqual(result.xp, Math.floor(100 * 1.5));
});
it('rollLoot — respects chance percentages', () => {
const rng = mulberry32(1);
const candidates = [
{ itemId: 1, chancePct: 100, qtyMin: 1, qtyMax: 1 },
{ itemId: 2, chancePct: 100, qtyMin: 1, qtyMax: 1 },
{ itemId: 3, chancePct: 100, qtyMin: 1, qtyMax: 1 },
];
const drops = rollLoot(candidates, rng);
// Max 2 distinct drops
assert.ok(drops.length <= 2);
});
it('rollLoot — max 2 distinct drops', () => {
const rng = mulberry32(1);
const candidates = Array.from({ length: 10 }, (_, i) => ({
itemId: i + 1, chancePct: 100, qtyMin: 1, qtyMax: 1,
}));
const drops = rollLoot(candidates, rng);
assert.ok(drops.length <= 2);
// All distinct
const itemIds = drops.map(d => d.itemId);
assert.strictEqual(itemIds.length, new Set(itemIds).size);
});
it('rollLoot — zero chance yields nothing', () => {
const rng = mulberry32(1);
const candidates = [
{ itemId: 1, chancePct: 0, qtyMin: 1, qtyMax: 1 },
];
const drops = rollLoot(candidates, rng);
assert.strictEqual(drops.length, 0);
});
it('materialSellPrice — common 2×level', () => {
assert.strictEqual(materialSellPrice('common', 5), 10);
});
it('materialSellPrice — uncommon 4×level', () => {
assert.strictEqual(materialSellPrice('uncommon', 5), 20);
});
it('materialSellPrice — rare 8×level', () => {
assert.strictEqual(materialSellPrice('rare', 14), 112);
});
it('materialSellPrice — minimum 1', () => {
assert.strictEqual(materialSellPrice('common', 0), 1);
});
});
describe('leveling', () => {
it('xpToNext — level 1→2 is 100', () => {
assert.strictEqual(xpToNext(1), 100);
});
it('xpToNext — level 2→3 is 282', () => {
assert.strictEqual(xpToNext(2), 282);
});
it('xpToNext — level 5→6 is 1118', () => {
assert.strictEqual(xpToNext(5), 1118);
});
it('xpToNext — level 10→11 is 3162', () => {
assert.strictEqual(xpToNext(10), 3162);
});
it('checkLevelUp — no level up', () => {
const result = checkLevelUp(1, 50, 10);
assert.strictEqual(result.newLevel, 1);
assert.strictEqual(result.leveledUp, false);
assert.strictEqual(result.totalXp, 60);
assert.strictEqual(result.unspentPoints, 0);
});
it('checkLevelUp — exact threshold', () => {
const result = checkLevelUp(1, 90, 10);
assert.strictEqual(result.newLevel, 2);
assert.strictEqual(result.leveledUp, true);
assert.strictEqual(result.totalXp, 0);
assert.strictEqual(result.unspentPoints, 3);
});
it('checkLevelUp — multiple level ups', () => {
// L1 needs 100 to reach L2, L2 needs 282 to reach L3
const result = checkLevelUp(1, 0, 100 + 282);
assert.strictEqual(result.newLevel, 3);
assert.strictEqual(result.leveledUp, true);
assert.strictEqual(result.totalXp, 0);
assert.strictEqual(result.unspentPoints, 6);
});
it('checkLevelUp — caps at max level 50', () => {
const result = checkLevelUp(50, 9999, 100);
assert.strictEqual(result.newLevel, 50);
assert.strictEqual(result.leveledUp, false);
});
it('allocatePoints — valid allocation', () => {
const result = allocatePoints(8, 4, 2, 8, 3, { str: 2, vit: 1 });
assert.strictEqual(result.str, 10);
assert.strictEqual(result.vit, 9);
assert.strictEqual(result.agi, 4);
assert.strictEqual(result.int, 2);
assert.strictEqual(result.remainingPoints, 0);
});
it('allocatePoints — invalid key throws', () => {
assert.throws(() => allocatePoints(8, 4, 2, 8, 3, { dex: 3 }), /Invalid stat/);
});
it('allocatePoints — wrong total throws', () => {
assert.throws(() => allocatePoints(8, 4, 2, 8, 3, { str: 1 }), /Allocated 1 but had 3/);
});
it('allocatePoints — non-integer throws', () => {
assert.throws(() => allocatePoints(8, 4, 2, 8, 3, { str: 1.5 }), /positive integer/);
});
});