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:
+359
@@ -0,0 +1,359 @@
|
||||
// ═══ 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user