157 lines
5.4 KiB
JavaScript
157 lines
5.4 KiB
JavaScript
// 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 }), /non-negative integer/);
|
||
});
|
||
});
|