Files
siege-protocol/js/upgrades.js
T
44r0n7 622a9fd170 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>
2026-06-16 11:36:53 -04:00

360 lines
16 KiB
JavaScript

// ═══ upgrades.js ═══
// ============================================================
// UPGRADES.JS — Purchase, refund, apply upgrade logic
// ============================================================
function collectUpgradeDependents(tree, upgradeId) {
const result = [];
function collect(id) {
for (const u of tree) {
if (u.requires.includes(id) && !result.includes(u.id)) {
result.push(u.id);
collect(u.id);
}
}
}
collect(upgradeId);
return result;
}
function getTowerDependents(upgradeId) {
return collectUpgradeDependents(TOWER_UPGRADE_TREE, upgradeId);
}
function getWeaponDependents(defId, upgradeId) {
return collectUpgradeDependents(WEAPON_UPGRADE_TREES[defId] || [], upgradeId);
}
function getShieldDependents(upgradeId) {
const type = G.tower.shield;
return type ? collectUpgradeDependents(SHIELD_UPGRADE_TREES[type] || [], upgradeId) : [];
}
function buyTowerUpgrade(upgradeId) {
const upgrade = TOWER_UPGRADE_TREE.find(u => u.id === upgradeId);
if (!upgrade) return;
if (!upgrade.repeatable && G.towerUpgradesBought.includes(upgradeId)) return;
if (upgrade.effect?.repair && G.tower.hp >= G.tower.maxHp) {
addLog('Tower already at full HP.', 'info');
return;
}
for (const req of upgrade.requires) {
if (!G.towerUpgradesBought.includes(req)) return;
}
if (spendableCredits() < upgrade.cost) return;
G.credits -= upgrade.cost;
if (!upgrade.repeatable) G.towerUpgradesBought.push(upgradeId);
applyTowerUpgrade(upgrade.effect);
sfx_buy();
addLog('Upgraded: ' + upgrade.label, 'info');
updateHUD();
renderShop();
}
function refundTowerUpgrade(upgradeId) {
const upgrade = TOWER_UPGRADE_TREE.find(u => u.id === upgradeId);
if (!upgrade || !G.towerUpgradesBought.includes(upgradeId)) return;
const toRefund = [upgradeId, ...getTowerDependents(upgradeId)]
.filter(id => G.towerUpgradesBought.includes(id));
let total = 0;
for (const id of toRefund) {
const u = TOWER_UPGRADE_TREE.find(x => x.id === id);
if (!u) continue;
total += u.cost;
reverseTowerUpgrade(u.effect, id);
G.towerUpgradesBought = G.towerUpgradesBought.filter(x => x !== id);
}
G.credits += total;
sfx_refund();
addLog('Refunded ' + toRefund.length + ' upgrade(s) — +' + total + 'c', 'info');
updateHUD();
renderShop();
}
function applyTowerUpgrade(effect) {
if (effect.maxHp) { G.tower.maxHp += effect.maxHp; G.tower.hp += effect.maxHp; }
if (effect.armor) G.tower.armor += effect.armor;
if (effect.aimSpeed) G.tower.aimSpeed += effect.aimSpeed;
if (effect.repair) G.tower.hp = Math.min(G.tower.maxHp, G.tower.hp + effect.repair);
if (effect.weaponSlot) G.tower.weaponSlots = Math.max(G.tower.weaponSlots, effect.weaponSlot);
if (effect.range) G.tower.range += effect.range;
if (effect.shield) { G.tower.shield = effect.shield; initShield(effect.shield); G.shieldUpgradesBought = []; }
}
function reverseTowerUpgrade(effect, id) {
if (effect.maxHp) { G.tower.maxHp -= effect.maxHp; G.tower.hp = Math.min(G.tower.hp, G.tower.maxHp); }
if (effect.armor) G.tower.armor = Math.max(0, G.tower.armor - effect.armor);
if (effect.aimSpeed) G.tower.aimSpeed = Math.max(0.01, G.tower.aimSpeed - effect.aimSpeed);
if (effect.range) G.tower.range = Math.max(150, G.tower.range - effect.range);
if (effect.weaponSlot) {
const remaining = G.towerUpgradesBought
.filter(x => x !== id)
.map(x => TOWER_UPGRADE_TREE.find(u => u.id === x))
.filter(u => u && u.effect && u.effect.weaponSlot)
.map(u => u.effect.weaponSlot);
G.tower.weaponSlots = remaining.length > 0 ? Math.max(...remaining) : 1;
while (G.weapons.filter(w=>w).length > G.tower.weaponSlots) G.weapons.pop();
}
if (effect.shield) {
G.tower.shield = null;
G.tower.shieldHp = 0;
G.tower.shieldMaxHp = 0;
G.shieldUpgradesBought = [];
}
}
function initShield(type) {
if (type === 'dome') {
G.tower.shieldMaxHp = 10; G.tower.shieldHp = 10;
G.tower.shieldRechargeDelay = 300; G.tower.shieldRechargeTimer = 0;
G.tower.shieldRechargeRate = 1; G.tower.shieldReflect = 0;
} else if (type === 'directional') {
G.tower.shieldMaxHp = 35; G.tower.shieldHp = 35;
G.tower.shieldArcWidth = Math.PI * 0.6; G.tower.shieldTrackSpeed = 0.06;
G.tower.shieldAngle = 0; G.tower.shieldAbsorption = 1.0;
G.tower.shieldRechargeDelay = 240; G.tower.shieldRechargeTimer = 0;
}
}
function buyWeapon(defId) {
const def = WEAPON_DEFS.find(w => w.id === defId);
if (!def) return;
if (!canBuyWeaponType(defId)) {
addLog(`${def.name} limit reached (${MAX_WEAPONS_PER_TYPE}/${MAX_WEAPONS_PER_TYPE}).`, 'lose');
return;
}
if (spendableCredits() < def.cost) return;
G.credits -= def.cost;
const instance = makeWeaponInstance(defId);
G.weaponUpgradesBought[instance.instanceId] = [];
sfx_buy();
const equippedCount = getEquippedWeapons().length;
if (equippedCount < G.tower.weaponSlots) {
let placedAt = -1;
for (let i = 0; i < G.tower.weaponSlots; i++) {
if (!G.weapons[i]) {
G.weapons[i] = instance;
placedAt = i;
break;
}
}
if (placedAt >= 0) addLog('Purchased ' + def.name + ' — equipped in slot ' + (placedAt + 1) + '!', 'win');
else addLog('Purchased ' + def.name + ' — equipped!', 'win');
} else {
// All slots full — add to inventory
G.weaponInventory = G.weaponInventory || [];
G.weaponInventory.push(instance);
addLog('Purchased ' + def.name + ' — stored in inventory. Open inventory.', 'win');
}
updateHUD();
renderShop();
}
function buyWeaponUpgrade(instanceId, upgradeId) {
const weapon = findWeaponInstance(instanceId);
if (!weapon) return;
const tree = WEAPON_UPGRADE_TREES[weapon.defId];
if (!tree) return;
const upgrade = tree.find(u => u.id === upgradeId);
if (!upgrade) return;
const bought = G.weaponUpgradesBought[instanceId] || [];
if (bought.includes(upgradeId)) return;
for (const req of upgrade.requires) { if (!bought.includes(req)) return; }
if (spendableCredits() < upgrade.cost) return;
G.credits -= upgrade.cost;
bought.push(upgradeId);
G.weaponUpgradesBought[instanceId] = bought;
applyWeaponUpgrade(weapon, upgrade.effect);
sfx_buy();
addLog(getWeaponDef(weapon).name + ': ' + upgrade.label, 'info');
updateHUD();
renderShop();
}
function refundWeaponUpgrade(instanceId, upgradeId) {
const weapon = findWeaponInstance(instanceId);
if (!weapon) return;
const tree = WEAPON_UPGRADE_TREES[weapon.defId] || [];
const bought = G.weaponUpgradesBought[instanceId] || [];
if (!bought.includes(upgradeId)) return;
const toRefund = [upgradeId, ...getWeaponDependents(weapon.defId, upgradeId)]
.filter(id => bought.includes(id));
let total = 0;
for (const id of toRefund) {
const u = tree.find(x => x.id === id);
if (!u) continue;
total += u.cost;
reverseWeaponUpgrade(weapon, u.effect);
G.weaponUpgradesBought[instanceId] = G.weaponUpgradesBought[instanceId].filter(x => x !== id);
}
G.credits += total;
sfx_refund();
addLog('Refunded ' + toRefund.length + ' upgrade(s) — +' + total + 'c', 'info');
updateHUD();
renderShop();
}
function applyWeaponUpgrade(weapon, effect) {
if (effect.damage) weapon.damage += effect.damage;
if (effect.fireRate) weapon.fireRate = Math.max(4, weapon.fireRate + effect.fireRate);
if (effect.projectileSpeed) weapon.projectileSpeed += effect.projectileSpeed;
if (effect.pierce) weapon.pierce = (weapon.pierce||0) + effect.pierce;
if (effect.critChance) weapon.critChance = (weapon.critChance||0) + effect.critChance;
if (effect.range) weapon.range += effect.range;
if (effect.coneAngle) weapon.coneAngle += effect.coneAngle;
if (effect.dotDuration) weapon.dotDuration = (weapon.dotDuration||0) + effect.dotDuration;
if (effect.chains) weapon.chains = (weapon.chains||0) + effect.chains;
if (effect.chainRange) weapon.chainRange = (weapon.chainRange||0) + effect.chainRange;
if (effect.aoeRadius) weapon.aoeRadius = (weapon.aoeRadius||0) + effect.aoeRadius;
if (effect.freezeDuration) weapon.freezeDuration = (weapon.freezeDuration||0) + effect.freezeDuration;
if (effect.armorShred) weapon.armorShred = (weapon.armorShred||0) + effect.armorShred;
if (effect.targets) weapon.targets = (weapon.targets||1) + effect.targets;
if (effect.amplify) weapon.amplify = (weapon.amplify||0) + effect.amplify;
if (effect.dotDamage) weapon.dotDamage = (weapon.dotDamage||0) + effect.dotDamage;
if (effect.projectileRadius) weapon.projectileRadius = (weapon.projectileRadius||4) + effect.projectileRadius;
if (effect.canInfuse) weapon.canInfuse = true;
if (effect.canInfuse2) weapon.canInfuse2 = true;
if (effect.canInfuse3) weapon.canInfuse3 = true;
}
function reverseWeaponUpgrade(weapon, effect) {
if (effect.damage) weapon.damage = Math.max(1, weapon.damage - effect.damage);
if (effect.fireRate) weapon.fireRate = Math.min(600, weapon.fireRate - effect.fireRate);
if (effect.projectileSpeed) weapon.projectileSpeed = Math.max(1, weapon.projectileSpeed - effect.projectileSpeed);
if (effect.pierce) weapon.pierce = Math.max(0, (weapon.pierce||0) - effect.pierce);
if (effect.critChance) weapon.critChance = Math.max(0, (weapon.critChance||0) - effect.critChance);
if (effect.range) weapon.range = Math.max(80, weapon.range - effect.range);
if (effect.coneAngle) weapon.coneAngle = Math.max(0.1, weapon.coneAngle - effect.coneAngle);
if (effect.dotDuration) weapon.dotDuration = Math.max(0, (weapon.dotDuration||0) - effect.dotDuration);
if (effect.chains) weapon.chains = Math.max(0, (weapon.chains||0) - effect.chains);
if (effect.chainRange) weapon.chainRange = Math.max(40, (weapon.chainRange||0) - effect.chainRange);
if (effect.aoeRadius) weapon.aoeRadius = Math.max(10, (weapon.aoeRadius||0) - effect.aoeRadius);
if (effect.freezeDuration) weapon.freezeDuration = Math.max(0, (weapon.freezeDuration||0) - effect.freezeDuration);
if (effect.armorShred) weapon.armorShred = Math.max(0, (weapon.armorShred||0) - effect.armorShred);
if (effect.targets) weapon.targets = Math.max(1, (weapon.targets||1) - effect.targets);
if (effect.amplify) weapon.amplify = Math.max(0, (weapon.amplify||0) - effect.amplify);
if (effect.dotDamage) weapon.dotDamage = Math.max(0, (weapon.dotDamage||0) - effect.dotDamage);
if (effect.projectileRadius) weapon.projectileRadius = Math.max(2, (weapon.projectileRadius||4) - effect.projectileRadius);
if (effect.canInfuse) { weapon.canInfuse = false; weapon.elements[0] = null; weapon.elements = weapon.elements.filter(Boolean); }
if (effect.canInfuse2) { weapon.canInfuse2 = false; weapon.elements[1] = null; weapon.elements = weapon.elements.filter(Boolean); }
if (effect.canInfuse3) { weapon.canInfuse3 = false; weapon.elements[2] = null; weapon.elements = weapon.elements.filter(Boolean); }
}
function buyShieldUpgrade(upgradeId) {
const type = G.tower.shield;
if (!type) return;
const tree = SHIELD_UPGRADE_TREES[type];
if (!tree) return;
const upgrade = tree.find(u => u.id === upgradeId);
if (!upgrade) return;
const bought = G.shieldUpgradesBought || [];
if (bought.includes(upgradeId)) return;
for (const req of upgrade.requires) { if (!bought.includes(req)) return; }
if (spendableCredits() < upgrade.cost) return;
G.credits -= upgrade.cost;
bought.push(upgradeId);
G.shieldUpgradesBought = bought;
applyShieldUpgrade(upgrade.effect);
sfx_buy();
addLog('Shield: ' + upgrade.label, 'info');
updateHUD();
renderShop();
}
function refundShieldUpgrade(upgradeId) {
const type = G.tower.shield;
if (!type) return;
const tree = SHIELD_UPGRADE_TREES[type] || [];
const bought = G.shieldUpgradesBought || [];
if (!bought.includes(upgradeId)) return;
const toRefund = [upgradeId, ...getShieldDependents(upgradeId)]
.filter(id => bought.includes(id));
let total = 0;
for (const id of toRefund) {
const u = tree.find(x => x.id === id);
if (!u) continue;
total += u.cost;
reverseShieldUpgrade(u.effect);
G.shieldUpgradesBought = G.shieldUpgradesBought.filter(x => x !== id);
}
G.credits += total;
sfx_refund();
addLog('Shield refund: +' + total + 'c', 'info');
updateHUD();
renderShop();
}
function applyShieldUpgrade(effect) {
if (effect.capacity) { G.tower.shieldMaxHp += effect.capacity; G.tower.shieldHp += effect.capacity; }
if (effect.rechargeDelay) G.tower.shieldRechargeDelay = Math.max(60, (G.tower.shieldRechargeDelay||300) + effect.rechargeDelay);
if (effect.reflect) G.tower.shieldReflect = (G.tower.shieldReflect||0) + effect.reflect;
if (effect.arcWidth) G.tower.shieldArcWidth += effect.arcWidth;
if (effect.trackSpeed) G.tower.shieldTrackSpeed += effect.trackSpeed;
if (effect.absorption) G.tower.shieldAbsorption = (G.tower.shieldAbsorption||1.0) + effect.absorption;
}
function reverseShieldUpgrade(effect) {
if (effect.capacity) { G.tower.shieldMaxHp = Math.max(1, G.tower.shieldMaxHp - effect.capacity); G.tower.shieldHp = Math.min(G.tower.shieldHp, G.tower.shieldMaxHp); }
if (effect.rechargeDelay) G.tower.shieldRechargeDelay = Math.min(600, (G.tower.shieldRechargeDelay||300) - effect.rechargeDelay);
if (effect.reflect) G.tower.shieldReflect = Math.max(0, (G.tower.shieldReflect||0) - effect.reflect);
if (effect.arcWidth) G.tower.shieldArcWidth = Math.max(0.3, G.tower.shieldArcWidth - effect.arcWidth);
if (effect.trackSpeed) G.tower.shieldTrackSpeed = Math.max(0.01, G.tower.shieldTrackSpeed - effect.trackSpeed);
if (effect.absorption) G.tower.shieldAbsorption = Math.max(1.0, (G.tower.shieldAbsorption||1.0) - effect.absorption);
}
function setWeaponInfusion(instanceId, slotIndex, element) {
const weapon = findWeaponInstance(instanceId);
if (!weapon) return;
if (slotIndex === 0 && !weapon.canInfuse) return;
if (slotIndex === 1 && !weapon.canInfuse2) return;
if (slotIndex === 2 && !weapon.canInfuse3) return;
weapon.elements[slotIndex] = element;
renderShop();
}
function setWeaponTargeting(instanceId, targeting) {
const weapon = findWeaponInstance(instanceId);
if (!weapon) return;
weapon.targeting = targeting;
}
function updateShield() {
if (!G.tower.shield) return;
if (G.tower.shield === 'directional') {
const nearest = [];
const cx = canvas.width/2, cy = canvas.height/2;
for (const e of G.enemies) {
if (!e.alive || e.spawnImmunity > 0) continue;
const d = distSq(e.x, e.y, cx, cy);
let insertAt = nearest.length;
while (insertAt > 0 && nearest[insertAt - 1].d > d) insertAt--;
if (insertAt >= 3) continue;
nearest.splice(insertAt, 0, { e, d });
if (nearest.length > 3) nearest.length = 3;
}
if (nearest.length > 0) {
let avgX = 0;
let avgY = 0;
for (const item of nearest) {
avgX += item.e.x;
avgY += item.e.y;
}
avgX /= nearest.length;
avgY /= nearest.length;
const desired = Math.atan2(avgY-canvas.height/2, avgX-canvas.width/2);
const delta = normalizeAngle(desired - G.tower.shieldAngle);
G.tower.shieldAngle += Math.sign(delta)*Math.min(Math.abs(delta), G.tower.shieldTrackSpeed||0.06);
}
}
if (G.tower.shieldHp < G.tower.shieldMaxHp) {
G.tower.shieldRechargeTimer = (G.tower.shieldRechargeTimer||0) + 1;
if (G.tower.shieldRechargeTimer >= (G.tower.shieldRechargeDelay||300)) {
G.tower.shieldHp = Math.min(G.tower.shieldMaxHp, G.tower.shieldHp + 1);
}
} else {
G.tower.shieldRechargeTimer = 0;
}
}