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:
+230
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user