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:
2026-06-16 11:36:53 -04:00
commit 622a9fd170
31 changed files with 6164 additions and 0 deletions
+153
View File
@@ -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++;
}