// ═══ state.js ═══ // ============================================================ // STATE.JS — Central game state, constants, definitions // ============================================================ // ── ARENA ───────────────────────────────────────────── const ARENA_RADIUS = 720; // play area radius; enemies always spawn at this distance from tower const ARENA_CX = 665; // center x of play area: (1600 - 270) / 2 const ARENA_CY = 482; // center y of play area: 64 + (900 - 64) / 2 // ── INITIAL GAME STATE ──────────────────────────────────────── function makeGameState() { return { credits: 150, score: 0, totalKills: 0, frame: 0, paused: false, gameOver: false, isBankrupt: false, _isNewBest: false, creditReserve: 50, // minimum credits kept in reserve — cannot spend upgrades below this // Camera camera: { zoom: 0.85, minZoom: 0.57, maxZoom: 1.8 }, // Tower tower: { hp: 20, maxHp: 20, armor: 0, aimSpeed: 0.026, range: 250, // vision + targeting radius; enemies outside are in fog and untargetable cannonAngle: 0, weaponSlots: 1, shield: null, shieldHp: 0, shieldMaxHp: 0, shieldAngle: 0, shieldUnlocked: [], }, // Weapons: array of active weapon instances weapons: [ makeWeaponInstance('cannon'), ], // Upgrade tracking towerUpgradesBought: [], // array of upgrade ids weaponUpgradesBought: {}, // { weaponInstanceId: [upgradeIds] } shieldUpgradesBought: [], // upgrade ids for current shield // Entities enemies: [], projectiles: [], beams: [], chainArcs: [], // lightning bolt visuals aoeZones: [], // lingering AoE (poison clouds, freeze zones) particles: [], floaters: [], portals: [], // Portal system portalCooldown: 0, // Input state selectedEnemyType: null, sendQuantity: 1, // Overlay panels — armory (weapons) and command (tower upgrades) armoryOpen: false, commandOpen: false, weaponDetailSlot: -1, // Entity id counter nextId: 1, // Kill streak streak: { count: 0, lastKillFrame: -999 }, // Enemy freshness (novelty bonus) — higher = less fresh = less bonus enemyFreshness: Object.fromEntries(ENEMY_DEFS.map(d => [d.id, 0])), // Difficulty / Prestige difficultyTier: 0, unlockedTiers: [0], prestigeLevel: 0, permanentBonuses: {}, // Other overlay panels threatOpen: false, prestigeOpen: false, }; } let G = makeGameState(); // global game state function countOwnedWeaponType(defId) { let count = 0; for (let i = 0; i < G.tower.weaponSlots; i++) { const w = G.weapons[i]; if (w && w.defId === defId) count++; } for (const w of (G.weaponInventory || [])) { if (w && w.defId === defId) count++; } return count; } function canBuyWeaponType(defId) { return countOwnedWeaponType(defId) < MAX_WEAPONS_PER_TYPE; } function makeWeaponInstance(defId) { const def = WEAPON_DEFS.find(w => w.id === defId); if (!def) return null; return { instanceId: 'w_' + defId + '_' + Date.now(), defId, defaultElement: def.defaultElement ?? 'physical', // Live stats (modified by upgrades) damage: def.damage, fireRate: def.fireRate, range: def.range ?? 9999, projectileSpeed: def.projectileSpeed ?? 6, projectileRadius: def.projectileRadius ?? 4, coneAngle: def.coneAngle ?? 0, chains: def.chains ?? 0, chainRange: def.chainRange ?? 0, aoeRadius: def.aoeRadius ?? 0, freezeDuration: def.freezeDuration ?? 0, armorShred: def.armorShred ?? 0, targets: def.targets ?? 1, amplify: def.amplify ?? 0, dotDamage: def.dotDamage ?? 0, dotInterval: def.dotInterval ?? 0, dotDuration: def.dotDuration ?? 0, pierce: 0, critChance: 0, elements: [], // no elements by default — must purchase infuse slots targeting: def.targeting, cooldown: 0, aimAngle: -Math.PI / 2, recoil: 0, muzzleFlash: 0, lastFireFrame: -9999, canInfuse: false, canInfuse2: false, canInfuse3: false, }; } function getWeaponDef(instanceOrId) { const id = typeof instanceOrId === 'string' ? instanceOrId : instanceOrId.defId; return WEAPON_DEFS.find(w => w.id === id); } function getWeaponElements(weapon) { const base = weapon.defaultElement || getWeaponDef(weapon)?.defaultElement || 'physical'; const elements = [...new Set([base, ...(weapon.elements || [])].filter(el => el && el !== 'physical'))]; return elements.length ? elements : ['physical']; } function uid() { return G.nextId++; }