Initial commit: Siege Protocol
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>
This commit is contained in:
+153
@@ -0,0 +1,153 @@
|
||||
// ═══ 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++;
|
||||
}
|
||||
Reference in New Issue
Block a user