622a9fd170
Inverted tower-defense browser game — deploy enemies yourself, tower auto-kills them, pocket credits, upgrade weapons. HTML + Canvas + vanilla JS, no build step. Co-Authored-By: claude-flow <ruv@ruv.net>
154 lines
4.5 KiB
JavaScript
154 lines
4.5 KiB
JavaScript
// ═══ state.js ═══
|
|
// ============================================================
|
|
// STATE.JS — Central game state, constants, definitions
|
|
// ============================================================
|
|
|
|
// ── ARENA ─────────────────────────────────────────────
|
|
const ARENA_RADIUS = 400; // 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
|
|
|
|
// 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,
|
|
|
|
// Shop
|
|
shopOpen: false,
|
|
shopTab: 'tower', // 'tower' | 'weapons' | weapon instance id
|
|
shopTreeWeapon: null,
|
|
|
|
// 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])),
|
|
};
|
|
}
|
|
|
|
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++;
|
|
}
|