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:
2026-06-17 11:58:17 -04:00
parent 6a710c3f03
commit 626879ed0c
21 changed files with 1884 additions and 312 deletions
+76 -12
View File
@@ -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) {