const fs = require("fs"); const vm = require("vm"); const path = require("path"); const assert = require("assert"); const source = fs.readFileSync(path.join(__dirname, "..", "runtime", "build", "index.js"), "utf8"); const context = { console, Math, Date, JSON, isFinite }; vm.createContext(context); vm.runInContext(source, context); function duel(towerType, enemyType, wave = 1, boss = false, difficulty = 1) { const base = context.ENEMIES[enemyType]; const waveScale = (1 + Math.max(0, wave - 1) * 0.20 + Math.max(0, wave - 3) * 0.09 + (difficulty === 2 ? Math.max(0, wave - 5) * 0.055 : 0)) * (boss ? 3.25 : 1) * context.DIFFICULTIES[difficulty].hp; const hullScale = waveScale * (boss && wave === 5 ? 0.5 : 1); const shielded = enemyType === 4 || (boss && wave === 5); const shield = shielded ? base.hp * waveScale * 0.55 * (boss && wave === 5 ? 0.5 : 1) : 0; const testedEnemy = { id: 1, type: enemyType, x: -40, y: 120, path_index: 1, route_id: 0, hp: base.hp * hullScale, max_hp: base.hp * hullScale, speed: base.speed * (1 + wave * 0.012) * (boss ? 0.82 : 1) * context.DIFFICULTIES[difficulty].speed, reward: base.reward, shield: shield > 0, shield_hp: shield, max_shield_hp: shield, shield_resistance: base.shieldResistance, boss }; const state = { wave, balance: { towers: context.TOWERS, enemies: context.ENEMIES }, towers: [{ id: 1, type: towerType, x: 540, y: 220, owner: 0, level: 1, cooldown: 0, angle: 0 }], enemies: [testedEnemy], shots: [], credits: [0, 0], reactorLives: 30, gameOver: false, waveActive: true, spawnRemaining: 0 }; const tick = 0.1; let elapsed = 0; for (; elapsed < 90 && state.enemies.length; elapsed += tick) { context.updateEnemies(state, tick); context.updateTowers(state, tick); } const alive = state.enemies.length > 0; const breached = state.reactorLives < 30; const remainingHp = Math.max(0, testedEnemy.hp); return { alive, killed: !alive && !breached && remainingHp <= 0, remainingHp, seconds: elapsed, breached, damageTaken: base.hp * hullScale - remainingHp }; } const rows = []; let evaluated = 0; for (let tower = 0; tower < context.TOWERS.length; tower++) { const towerData = context.TOWERS[tower]; const results = []; for (let enemy = 0; enemy < context.ENEMIES.length; enemy++) { const enemyData = context.ENEMIES[enemy]; if (towerData.targetClass !== "both" && towerData.targetClass !== enemyData.category) continue; const regular = duel(tower, enemy); const late = duel(tower, enemy, 15, false, 2); evaluated++; results.push(`${enemy}:${regular.killed ? `kill ${regular.seconds.toFixed(1)}s` : `leaks ${Math.round(regular.remainingHp)} HP`}/${late.killed ? `H W15 kill ${late.seconds.toFixed(1)}s` : `H W15 leaks ${Math.round(late.remainingHp)} HP`}`); } rows.push(`${towerData.targetClass.padEnd(6)} tower ${tower}: ${results.join(" | ")}`); } const bossResults = []; for (let tower = 0; tower < context.TOWERS.length; tower++) { if (context.TOWERS[tower].targetClass !== "air" && context.TOWERS[tower].targetClass !== "both") continue; const result = duel(tower, 5, 5, true, 1); bossResults.push(`tower ${tower}: ${result.killed ? `killed in ${result.seconds.toFixed(1)}s` : `reaches reactor with ${Math.round(result.remainingHp)} HP`}`); } assert.ok(evaluated >= 20, "the matrix must cover all valid tower/enemy matchups"); console.log("BALANCE DUEL RESULTS (one level-1 tower, one enemy, fixed path and no reinvestment)"); console.log(rows.join("\n")); console.log("WAVE 5 FIRST BOSS (single-tower reference): " + bossResults.join(" | ")); console.log(`BALANCE_DUEL_TEST_OK matchups=${evaluated}`);