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
+230
View File
@@ -0,0 +1,230 @@
// ═══ elements.js ═══
// ============================================================
// ELEMENTS.JS — Elemental damage, debuffs, DoT
// ============================================================
// Apply elemental hit effects to an enemy
function applyElementalEffects(enemy, elements, rawDamage) {
let totalDamage = rawDamage;
const results = { damage: 0, critted: false, effects: [] };
for (const el of elements) {
if (!el) continue;
const res = enemy.resistances?.[el] ?? 1.0;
const weak = enemy.weaknesses?.[el] ?? 1.0;
const mult = res * weak;
switch (el) {
case 'fire':
applyDoT(enemy, 'burn', 'fire', 1, 30, 180, '#ff6b35');
results.effects.push('burn');
break;
case 'ice':
applySlow(enemy, 0.5, 120);
results.effects.push('slow');
break;
case 'lightning':
chainLightningProc(enemy, totalDamage * 0.5, 100);
results.effects.push('chain');
break;
case 'poison':
applyDoT(enemy, 'poison', 'poison', 1, 20, 240, '#7fff4f');
results.effects.push('poison');
break;
case 'void':
enemy.armor = Math.max(0, (enemy.armor || 0) - 1);
results.effects.push('shred');
break;
case 'arcane':
enemy.amplified = (enemy.amplified || 0) + 0.25;
results.effects.push('amplify');
break;
}
totalDamage *= mult;
}
results.damage = totalDamage;
return results;
}
function applyDoT(enemy, type, element, dmg, interval, duration, color) {
if (!enemy.dots) enemy.dots = [];
// Refresh or add
const existing = enemy.dots.find(d => d.type === type);
if (existing) {
existing.remaining = Math.max(existing.remaining, duration);
existing.dmg = Math.max(existing.dmg, dmg);
existing.element = element;
} else {
enemy.dots.push({ type, element, dmg, interval, remaining: duration, tick: 0, color });
}
}
function applySlow(enemy, factor, duration) {
if (!enemy.slow || enemy.slow.factor > factor) {
enemy.slow = { factor, remaining: duration };
} else {
enemy.slow.remaining = Math.max(enemy.slow.remaining, duration);
}
}
function applyFreeze(enemy, duration) {
// Diminishing returns: each successive freeze is shorter
// 60-frame immunity window after thawing
if (enemy._freezeImmune > 0) return;
const diminish = enemy._freezeCount ? Math.pow(0.75, enemy._freezeCount) : 1.0;
const actual = Math.max(30, Math.round(duration * diminish));
enemy.frozen = actual;
enemy._freezeCount = (enemy._freezeCount || 0) + 1;
if (!enemy.slow) enemy.slow = { factor: 0, remaining: actual };
enemy.slow.factor = 0;
enemy.slow.remaining = actual;
}
// Chain lightning proc — bounces to nearby enemies
function chainLightningProc(sourceEnemy, damage, range) {
if (!G || !G.enemies) return;
const rangeSq = range * range;
let seen = 0;
let target = null;
for (const e of G.enemies) {
if (!e.alive || e.id === sourceEnemy.id) continue;
if (distSq(e.x, e.y, sourceEnemy.x, sourceEnemy.y) >= rangeSq) continue;
seen++;
if (Math.random() < 1 / seen) target = e;
}
if (!target) return;
dealDamage(target, damage, ['lightning'], false);
// Spark visual
spawnChainArc(sourceEnemy.x, sourceEnemy.y, target.x, target.y, '#ffe033');
}
// Core damage dealer — handles armor, resist, amplify, crits
function dealDamage(enemy, rawDamage, elements = ['physical'], canCrit = false, critChance = 0, armorShred = 0) {
if (!enemy.alive) return 0;
// Armor shred
if (armorShred > 0) {
enemy.armor = Math.max(0, (enemy.armor || 0) - armorShred);
}
// Base damage after armor
let dmg = Math.max(1, rawDamage - (enemy.armor || 0));
// Elemental multipliers
let elemMult = 1;
for (const el of elements) {
if (!el || el === 'physical') continue;
const res = enemy.resistances?.[el] ?? 1.0;
const weak = enemy.weaknesses?.[el] ?? 1.0;
elemMult *= res * weak;
}
dmg *= elemMult;
// Amplify debuff
if (enemy.amplified) dmg *= (1 + enemy.amplified);
// Crit
let critted = false;
if (canCrit && Math.random() < critChance) {
dmg *= 2;
critted = true;
}
dmg = Math.round(dmg);
enemy.hp -= dmg;
enemy.hitFlash = 6;
// Show damage number for crits / elemental
const showNumber = critted || (elements.some(e => e && e !== 'physical'));
if (showNumber && dmg > 0) {
const color = critted ? '#ffffff' : (ELEMENTS[elements[0]]?.color || '#fff');
const label = critted ? `${dmg}!` : `${dmg}`;
spawnFloater(enemy.x, enemy.y - enemy.radius - 4, label, color, critted ? 1.4 : 1.0);
}
// Apply elemental side effects
for (const el of elements) {
if (!el || el === 'physical') continue;
applyElementalEffect(enemy, el);
}
return dmg;
}
function applyElementalEffect(enemy, el) {
switch (el) {
case 'fire':
applyDoT(enemy, 'burn', 'fire', 1, 30, 180, '#ff6b35');
break;
case 'ice':
applySlow(enemy, 0.5, 120);
break;
case 'poison':
applyDoT(enemy, 'poison', 'poison', 1, 20, 240, '#7fff4f');
break;
case 'void':
enemy.armor = Math.max(0, (enemy.armor || 0) - 1);
break;
case 'arcane':
enemy.amplified = Math.min(1.5, (enemy.amplified || 0) + 0.1);
break;
case 'lightning':
// chain handled separately in weapon logic
break;
}
}
// Tick DoTs and debuffs on an enemy each frame
function tickEnemyStatus(enemy) {
// DoTs
if (enemy.dots) {
for (const dot of enemy.dots) {
dot.tick++;
if (dot.tick >= dot.interval) {
dot.tick = 0;
const res = enemy.resistances?.[dot.element] ?? 1.0;
const weak = enemy.weaknesses?.[dot.element] ?? 1.0;
const dmg = Math.max(0, Math.round(dot.dmg * res * weak));
if (dmg > 0) {
enemy.hp -= dmg;
enemy.hitFlash = 3;
// Small particle tick
spawnParticle(enemy.x + (Math.random()-0.5)*8, enemy.y + (Math.random()-0.5)*8,
dot.color, 0, 0, 0.6, 3, 0.05);
}
}
dot.remaining--;
}
let write = 0;
for (let i = 0; i < enemy.dots.length; i++) {
const dot = enemy.dots[i];
if (dot.remaining > 0) enemy.dots[write++] = dot;
}
enemy.dots.length = write;
}
// Slow / freeze decay
if (enemy.frozen > 0) {
enemy.frozen--;
if (enemy.frozen === 0) {
if (enemy.slow) enemy.slow = null;
enemy._freezeImmune = 60; // 1 second immunity after thaw
}
}
if (enemy._freezeImmune > 0) enemy._freezeImmune--;
if (enemy.slow) {
enemy.slow.remaining--;
if (enemy.slow.remaining <= 0) enemy.slow = null;
}
// Hit flash decay
if (enemy.hitFlash > 0) enemy.hitFlash--;
// Amplify decay
if (enemy.amplified) {
enemy.amplified *= 0.995;
if (enemy.amplified < 0.01) enemy.amplified = 0;
}
}