Add freshness bar, enhance overlays and renderers
- Add enemy freshness tracking (novelty bonus for repeated deploys) - Add freshness bar to sidepanel enemy cards with penalty indicator - Major overhaul of renderer-overlays.js (790+ lines for UI polish) - Enhanced combat log, shop overlays, and inventory UI - Improved weapon/upgrade display with partial ownership colors - Added element icons and weakness/resistance indicators to cards - Enhanced radial menu and tooltip system - Add "stale/%" penalty text when freshness depleted - Update play link to ffazeshift.net in index.html
This commit is contained in:
+76
-12
@@ -4,7 +4,7 @@
|
||||
// ============================================================
|
||||
|
||||
// ── ENEMY SPAWNING ────────────────────────────────────────────
|
||||
function spawnEnemy(def, x, y, rewardOverride = null, offsetAngle = null, costOverride = null, breachRiskMult = 1) {
|
||||
function spawnEnemy(def, x, y, rewardOverride = null, offsetAngle = null, costOverride = null, breachRiskMult = 1, extraProps = null) {
|
||||
// For swarm units, offset position slightly around the portal
|
||||
let spawnX = x, spawnY = y;
|
||||
if (offsetAngle !== null) {
|
||||
@@ -12,18 +12,26 @@ function spawnEnemy(def, x, y, rewardOverride = null, offsetAngle = null, costOv
|
||||
spawnX = x + Math.cos(offsetAngle) * spread;
|
||||
spawnY = y + Math.sin(offsetAngle) * spread;
|
||||
}
|
||||
G.enemies.push({
|
||||
|
||||
// Apply tier multipliers — skip for echo copies which carry pre-scaled HP
|
||||
const tierDef = DIFFICULTY_TIERS[G.difficultyTier || 0];
|
||||
const skipScale = extraProps?._echoSplit;
|
||||
const scaledHp = skipScale ? def.hp : Math.round(def.hp * (tierDef?.hpMult ?? 1));
|
||||
const scaledSpeed = skipScale ? def.speed : def.speed * (tierDef?.speedMult ?? 1);
|
||||
const scaledArmor = skipScale ? (def.armor ?? 0) : Math.round((def.armor ?? 0) * (tierDef?.armorMult ?? 1));
|
||||
|
||||
const instance = {
|
||||
id: uid(),
|
||||
defId: def.id,
|
||||
name: def.name,
|
||||
x: spawnX, y: spawnY,
|
||||
hp: def.hp,
|
||||
maxHp: def.hp,
|
||||
speed: def.speed,
|
||||
baseSpeed: def.speed,
|
||||
hp: scaledHp,
|
||||
maxHp: scaledHp,
|
||||
speed: scaledSpeed,
|
||||
baseSpeed: scaledSpeed,
|
||||
radius: def.radius,
|
||||
armor: def.armor ?? 0,
|
||||
baseArmor: def.armor ?? 0,
|
||||
armor: scaledArmor,
|
||||
baseArmor: scaledArmor,
|
||||
evasion: def.evasion ?? 0,
|
||||
armorPen: def.armorPen ?? 0,
|
||||
color: def.color,
|
||||
@@ -46,7 +54,11 @@ function spawnEnemy(def, x, y, rewardOverride = null, offsetAngle = null, costOv
|
||||
angle: 0,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
});
|
||||
};
|
||||
|
||||
if (extraProps) Object.assign(instance, extraProps);
|
||||
|
||||
G.enemies.push(instance);
|
||||
}
|
||||
|
||||
// ── DEPLOY (player action) ────────────────────────────────────
|
||||
@@ -54,6 +66,10 @@ function deployEnemy(defId, quantity = 1) {
|
||||
const def = ENEMY_DEFS.find(e => e.id === defId);
|
||||
if (!def) return;
|
||||
|
||||
// Tier and prestige gate
|
||||
if ((def.minTier ?? 0) > (G.difficultyTier || 0)) return;
|
||||
if ((def.minPrestige ?? 0) > (G.prestigeLevel || 0)) return;
|
||||
|
||||
const totalCost = def.cost * quantity;
|
||||
if (G.credits < totalCost) return;
|
||||
if (G.gameOver) return;
|
||||
@@ -62,12 +78,20 @@ function deployEnemy(defId, quantity = 1) {
|
||||
|
||||
// Bonus reward multiplier and breach risk for sending multiples
|
||||
const bonusMult = quantity >= 50 ? 1.3 : quantity >= 25 ? 1.2 : quantity >= 10 ? 1.12 : quantity >= 5 ? 1.05 : 1.0;
|
||||
const rewardPerUnit = Math.round(def.reward * bonusMult);
|
||||
const tierDef = DIFFICULTY_TIERS[G.difficultyTier || 0];
|
||||
const tierRewardMult = tierDef?.rewardMult ?? 1;
|
||||
const rewardPerUnit = Math.round(def.reward * bonusMult * tierRewardMult);
|
||||
const breachRiskMult = quantity >= 50 ? 2.2 : quantity >= 25 ? 1.65 : quantity >= 10 ? 1.4 : quantity >= 5 ? 1.2 : 1.0;
|
||||
|
||||
// Freshness tracking — increment before deploy so bar reflects cost immediately
|
||||
G.enemyFreshness[defId] = (G.enemyFreshness[defId] || 0) + quantity;
|
||||
|
||||
// First-deploy tip for Void Herald
|
||||
if (def.id === 'voidherald' && !G._voidHeraldTipShown) {
|
||||
G._voidHeraldTipShown = true;
|
||||
addLog('VOID HERALD: Deploy in a crowd — shield activates only when 3+ weapons target it at once.', 'info');
|
||||
}
|
||||
|
||||
if (def.id === 'swarm') {
|
||||
// Each swarm card = one burst portal. Split rewards exactly across minis.
|
||||
const swarmUnitCost = def.cost / def.count;
|
||||
@@ -85,7 +109,8 @@ function deployEnemy(defId, quantity = 1) {
|
||||
const plural = quantity > 1 ? ` ×${quantity}` : '';
|
||||
const bonusStr = bonusMult > 1 ? ` (+${Math.round((bonusMult-1)*100)}% reward!)` : '';
|
||||
const riskStr = breachRiskMult > 1 ? ` [risk x${breachRiskMult.toFixed(2)}]` : '';
|
||||
addLog(`Deployed ${def.name}${plural} — ${totalCost}¢${bonusStr}${riskStr}`, 'info');
|
||||
const tierStr = tierRewardMult > 1 ? ` [${tierDef.name}: ×${tierRewardMult.toFixed(1)}]` : '';
|
||||
addLog(`Deployed ${def.name}${plural} — ${totalCost}¢${bonusStr}${tierStr}${riskStr}`, 'info');
|
||||
updateHUD();
|
||||
}
|
||||
|
||||
@@ -203,6 +228,24 @@ function updateEnemies() {
|
||||
const cx = ARENA_CX;
|
||||
const cy = ARENA_CY;
|
||||
|
||||
// Reset per-frame flags for special enemies
|
||||
for (const e of G.enemies) {
|
||||
if (!e.alive) continue;
|
||||
e.commanderAuraDR = 0;
|
||||
if (e.defId === 'voidherald') e._voidHitsThisFrame = 0;
|
||||
}
|
||||
|
||||
// Commander aura: all alive enemies within 80px gain +30% DR (including self)
|
||||
const auraRadSq = 80 * 80;
|
||||
for (const cmd of G.enemies) {
|
||||
if (!cmd.alive || cmd.defId !== 'commander') continue;
|
||||
for (const t of G.enemies) {
|
||||
if (t.alive && distSq(cmd.x, cmd.y, t.x, t.y) <= auraRadSq) {
|
||||
t.commanderAuraDR = 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of G.enemies) {
|
||||
if (!e.alive) continue;
|
||||
|
||||
@@ -250,6 +293,24 @@ function updateEnemies() {
|
||||
}
|
||||
|
||||
compactLiveArray(G.enemies, e => e.alive);
|
||||
|
||||
// Process Echo split queue — spawn 2 copies per pending split
|
||||
if (G._pendingEchoSpawns?.length) {
|
||||
const echoDef = ENEMY_DEFS.find(d => d.id === 'echo');
|
||||
const tierDef = DIFFICULTY_TIERS[G.difficultyTier || 0];
|
||||
const tierRewardMult = tierDef?.rewardMult ?? 1;
|
||||
for (const spawn of G._pendingEchoSpawns) {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const copyReward = Math.round((echoDef.echoReward ?? 120) * tierRewardMult);
|
||||
spawnEnemy(
|
||||
{ ...echoDef, hp: spawn.copyHp },
|
||||
spawn.x, spawn.y, copyReward,
|
||||
Math.random() * Math.PI * 2, echoDef.cost, 1, { _echoSplit: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
G._pendingEchoSpawns = [];
|
||||
}
|
||||
}
|
||||
|
||||
function killEnemy(enemy, giveReward) {
|
||||
@@ -359,7 +420,10 @@ function breachTower(enemy) {
|
||||
// Pick targeting for a weapon — only considers enemies within tower vision range
|
||||
function pickTarget(weapon) {
|
||||
const cx = ARENA_CX, cy = ARENA_CY;
|
||||
const towerRange = G.tower.range ?? 9999;
|
||||
const _activeWeapons = (G.weapons || []).filter(w => w);
|
||||
const towerRange = _activeWeapons.length > 0
|
||||
? Math.max(..._activeWeapons.map(w => w.range ?? 0))
|
||||
: 9999;
|
||||
const towerRangeSq = towerRange * towerRange;
|
||||
let targeting = weapon.targeting || 'nearest';
|
||||
switch (targeting) {
|
||||
|
||||
Reference in New Issue
Block a user