/* Neon Rampage — entities: player, enemies, bullets, pickups, particles. */ (function () { 'use strict'; var US = (window.US = window.US || {}); var R = US.util.rand; function hooks() { return US.entities._hooks || {}; } /* ---------------- Player ---------------- */ function Player(opts) { var hero = opts.hero; this.x = opts.x; this.y = opts.y; this.vx = 0; this.vy = 0; this.radius = 15; this.heroId = hero.id; this.skin = opts.skin; this.stats = { maxHp: hero.hp, speedMul: 1, dmgMul: hero.dmgMul, rofMul: 1, pellets: 0, // extra projectiles from cards pierce: 0, crit: hero.crit || 0.05, critMul: hero.critMul || 2, lifesteal: 0, bulletSpeedMul: 1, bulletSizeMul: 1, magnet: 90, explosive: 0, ricochet: 0, coinMul: 1, regen: 0, thorns: 0, ultRate: 1, startBombs: 0 }; // permanent amber upgrades persist across every run var UPS = US.UPGRADES || {}; var svUp = (US.save && US.save.get().upgrades) || {}; Object.keys(UPS).forEach(function (k) { var lv = svUp[k] | 0; if (lv > 0 && UPS[k].apply) UPS[k].apply(this.stats, lv); }, this); this.hp = this.stats.maxHp; this.speedBase = hero.speed; this.weapon = US.WEAPONS[opts.weaponId] || US.WEAPONS.pistol; this.cooldown = 0; this.bombs = opts.bombs || 0; this.ult = 0; this.invuln = 1.2; // spawn protection this.aim = 0; this.firing = false; this.cards = []; this.alive = true; } Player.prototype.takeCard = function (card) { card.apply(this.stats); if (card.onPick) card.onPick(this); this.cards.push(card.id); }; Player.prototype.dpsStats = function () { var s = this.stats, w = this.weapon; var gg = US.game && US.game.get ? US.game.get() : null; var fire = !!(gg && gg.onFire); return { dmg: w.dmg * s.dmgMul * (fire ? 2 : 1), rof: w.rof * s.rofMul * (fire ? 1.4 : 1), pellets: w.pellets + s.pellets }; }; /* ---------------- Enemy ---------------- */ function Enemy(def, x, y, mods) { mods = mods || {}; this.def = def; this.id = def.id; this.x = x; this.y = y; this.vx = 0; this.vy = 0; this.radius = def.radius * (mods.size || 1); this.maxHp = def.hp * (mods.hp || 1); this.hp = this.maxHp; this.speed = def.speed * (mods.speed || 1) * R(0.9, 1.12); this.contactDmg = def.contact * (mods.dmg || 1); this.elite = !!mods.elite; this.color = this.elite ? '#ff2e50' : def.color; this.hitFlash = 0; this.slowT = 0; this.touchCd = 0; this.shootCd = def.ranged ? R(0.5, 1.6) : 0; this.wobble = Math.random() * Math.PI * 2; this.face = 0; this.seed = Math.random() * 10; // boss state this.isBoss = !!mods.isBoss; this.patterns = def.patterns || []; this.patCd = 2; this.charging = 0; this.chargeDir = { x: 0, y: 0 }; this.splitStage = mods.splitStage || 0; this.spawnAnim = 0.6; this.alive = true; } Enemy.prototype.update = function (g, dt) { if (this.spawnAnim > 0) { this.spawnAnim -= dt; return; // brief spawn-in grace (drawn as growing ring) } var p = g.player; var dx = p.x - this.x, dy = p.y - this.y; var d = Math.hypot(dx, dy) || 1; var nx = dx / d, ny = dy / d; var sp = this.speed * (this.slowT > 0 ? 0.45 : 1); // tar pits bog everything down if (hooks().tarSlow && hooks().tarSlow(this.x, this.y)) sp *= 0.55; // beasts hunt with confidence under moonlight if (hooks().nightFast && hooks().nightFast()) sp *= 1.08; if (this.slowT > 0) this.slowT -= dt; var ranged = this.def.ranged; if (ranged && d < ranged.range && d > 120) { // strafe & shoot this.x += (-ny) * sp * 0.6 * dt + nx * sp * 0.2 * dt; this.y += (nx) * sp * 0.6 * dt + ny * sp * 0.2 * dt; this.face = Math.atan2(ny, nx); this.shootCd -= dt; if (this.shootCd <= 0) { this.shootCd = ranged.rof * R(0.8, 1.2); hooks().enemyBullet(this.x, this.y, nx, ny, ranged.speed, ranged.dmg, this.def.color); } } else if (this.isBoss) { this.updateBoss(g, dt, nx, ny, sp, d); } else if (this.def.dash) { // Pachycephalosaurus: telegraph, then head-butt charge var D = this.def.dash; if (this.dashing > 0) { this.dashing -= dt; this.x += Math.cos(this.face) * D.speed * dt; this.y += Math.sin(this.face) * D.speed * dt; } else { this.dashCd = (this.dashCd === undefined) ? R(1.2, D.cd) : this.dashCd - dt; this.wobble += dt * 3; var wobP = Math.sin(this.wobble) * 0.25; this.x += (nx - ny * wobP) * sp * 0.75 * dt; this.y += (ny + nx * wobP) * sp * 0.75 * dt; this.face = Math.atan2(ny + nx * wobP, nx - ny * wobP); if (this.dashCd <= 0 && d < 420) { this.dashing = D.dur; this.dashCd = D.cd; this.face = Math.atan2(ny, nx); // lock aim at wind-up end } } } else { // slight per-enemy weave so groups look organic this.wobble += dt * 3; var wob = Math.sin(this.wobble) * 0.25; this.x += (nx - ny * wob) * sp * dt; this.y += (ny + nx * wob) * sp * dt; this.face = Math.atan2(ny + nx * wob, nx - ny * wob); } if (this.charging > 0) { this.charging -= dt; this.x += this.chargeDir.x * this.speed * 4.2 * dt; this.y += this.chargeDir.y * this.speed * 4.2 * dt; this.face = Math.atan2(this.chargeDir.y, this.chargeDir.x); } if (this.hitFlash > 0) this.hitFlash -= dt; if (this.touchCd > 0) this.touchCd -= dt; // heavy beasts kick up dust as they stomp around if (this.radius >= 17 && Math.random() < dt * 2.5) { US.entities.spawnParticles(null, this.x, this.y, '#8f7448', 1, 0.8, 3.5); } // contact damage to player if (d < this.radius + p.radius && this.touchCd <= 0 && !p.invulnHit()) { p.hurt(this.contactDmg); this.touchCd = 0.5; if (p.stats.thorns > 0) g.damageEnemy(this, p.stats.thorns, false, true); } }; Enemy.prototype.updateBoss = function (g, dt, nx, ny, sp, dist) { var pats = this.patterns; this.patCd -= dt; if (this.charging <= 0) { this.x += nx * sp * dt; this.y += ny * sp * dt; } if (this.patCd > 0) return; var pick = pats[Math.floor(Math.random() * pats.length)]; switch (pick) { case 'charge': this.chargeDir = { x: nx, y: ny }; this.charging = 0.55; this.patCd = 3.2; break; case 'radial': { var n = 14; for (var i = 0; i < n; i++) { var a = (i / n) * Math.PI * 2 + Math.random() * 0.2; g.enemyBullet(this.x, this.y, Math.cos(a), Math.sin(a), 200, 12, this.color); } this.patCd = 3.4; break; } case 'aimed': { for (var b = -1; b <= 1; b++) { var ang = Math.atan2(ny, nx) + b * 0.18; g.enemyBullet(this.x, this.y, Math.cos(ang), Math.sin(ang), 320, 12, this.color); } this.patCd = 1.6; break; } case 'spiral': { var self = this; var count = 0; var iv = setInterval(function () { if (!self.alive || !US.game || US.game.over) { clearInterval(iv); return; } var a = count * 0.55; g.enemyBullet(self.x, self.y, Math.cos(a), Math.sin(a), 230, 11, self.color); if (++count >= 16) clearInterval(iv); }, 70); this.patCd = 4.5; break; } case 'spawn': { for (var s = 0; s < 3; s++) { var a2 = Math.random() * Math.PI * 2; hooks().spawnEnemy('grunt', this.x + Math.cos(a2) * 70, this.y + Math.sin(a2) * 70, { hp: 0.7 }); } this.patCd = 6; break; } case 'split': { if (this.splitStage === 0 && this.hp < this.maxHp * 0.5) { this.alive = false; // replaced below by children via game hook hooks().splitBoss(this); } else { this.patCd = 2; } break; } } }; /* ---------------- Bullets ---------------- */ function Bullet(x, y, ang, speed, dmg, color, size, owner, opts) { opts = opts || {}; this.x = x; this.y = y; this.vx = Math.cos(ang) * speed; this.vy = Math.sin(ang) * speed; this.dmg = dmg; this.color = color; this.size = size; this.owner = owner; // 'player' | 'enemy' this.life = opts.life || 1.6; this.pierceLeft = opts.pierce || 0; this.aoe = opts.aoe || 0; this.ricochet = opts.ricochet || 0; this.crit = !!opts.crit; this.kind = opts.kind || null; // sprite key for js/art.js this.hitIds = []; this.alive = true; } Bullet.prototype.update = function (g, dt) { this.x += this.vx * dt; this.y += this.vy * dt; this.life -= dt; if (this.life <= 0) { this.alive = false; return; } if (this.owner === 'player') { for (var i = 0; i < g.enemies.length; i++) { var e = g.enemies[i]; if (!e.alive || e.spawnAnim > 0) continue; if (this.hitIds.indexOf(e.eid) >= 0) continue; var dx = e.x - this.x, dy = e.y - this.y; if (dx * dx + dy * dy < (e.radius + this.size) * (e.radius + this.size)) { this.hitIds.push(e.eid); if (e.isFighter) e.lastHitBy = 0; // player kill credit hooks().damageEnemy(e, this.dmg, this.crit); if (this.aoe && e.alive !== undefined) hooks().explode(this.x, this.y, this.aoe, this.dmg * 0.6); if (this.ricochet > 0) { var nxt = hooks().nearestEnemy(this.x, this.y, 340, this.hitIds); if (nxt) { this.ricochet--; var a = Math.atan2(nxt.y - this.y, nxt.x - this.x); var spd = Math.hypot(this.vx, this.vy); this.vx = Math.cos(a) * spd; this.vy = Math.sin(a) * spd; this.life = Math.max(this.life, 0.8); this.hitIds.length = 0; this.hitIds.push(nxt.eid); return; } } if (this.pierceLeft > 0) { this.pierceLeft--; } else { this.alive = false; } break; } } } else { // enemy bullets hit the player… var p = g.player; var pdx = p.x - this.x, pdy = p.y - this.y; if (pdx * pdx + pdy * pdy < (p.radius + this.size) * (p.radius + this.size)) { if (!p.invulnHit()) p.hurt(this.dmg); this.alive = false; return; } // …and in the arena, bot bullets also hit other fighters if (typeof this.ownerEid === 'number' && g.mode === 'arena') { for (var fi = 0; fi < g.enemies.length; fi++) { var f = g.enemies[fi]; if (!f.alive || !f.isFighter || f.eid === this.ownerEid || f.spawnAnim > 0) continue; var fdx = f.x - this.x, fdy = f.y - this.y; if (fdx * fdx + fdy * fdy < (f.radius + this.size) * (f.radius + this.size)) { f.lastHitBy = this.ownerEid; hooks().damageEnemy(f, this.dmg, false, true); this.alive = false; break; } } } } }; /* ---------------- Pickups ---------------- */ function Pickup(x, y, kind, amount) { this.x = x; this.y = y; this.kind = kind; // coin | heal | bomb this.amount = amount || 1; this.life = kind === 'coin' ? 12 : 10; this.t = Math.random() * Math.PI * 2; this.vx = R(-40, 40); this.vy = R(-40, 40); this.alive = true; } Pickup.prototype.update = function (g, dt) { this.life -= dt; this.t += dt * 5; if (this.life <= 0) { this.alive = false; return; } var p = g.player; var dx = p.x - this.x, dy = p.y - this.y; var d = Math.hypot(dx, dy); var range = p.stats.magnet; if (d < range) { var pull = 260 + (range - d) * 3; this.x += (dx / d) * pull * dt; this.y += (dy / d) * pull * dt; } else { this.x += this.vx * dt; this.y += this.vy * dt; this.vx *= 0.92; this.vy *= 0.92; } if (d < p.radius + 10) { this.alive = false; hooks().collect(this); } }; /* ---------------- Particles ---------------- */ var particles = []; function spawnParticles(g, x, y, color, n, power, size) { for (var i = 0; i < n; i++) { if (particles.length > 600) particles.shift(); var a = Math.random() * Math.PI * 2; var v = R(30, 60) * power * (0.4 + Math.random()); particles.push({ x: x, y: y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: R(0.25, 0.7), maxLife: 0.7, color: color, size: size || 3 }); } } function updateParticles(dt) { for (var i = particles.length - 1; i >= 0; i--) { var pt = particles[i]; pt.x += pt.vx * dt; pt.y += pt.vy * dt; pt.vx *= 0.94; pt.vy *= 0.94; pt.life -= dt; if (pt.life <= 0) particles.splice(i, 1); } } function drawParticles(ctx) { for (var i = 0; i < particles.length; i++) { var pt = particles[i]; ctx.globalAlpha = Math.max(0, pt.life / pt.maxLife); ctx.fillStyle = pt.color; ctx.fillRect(pt.x - pt.size / 2, pt.y - pt.size / 2, pt.size, pt.size); } ctx.globalAlpha = 1; } function clearParticles() { particles.length = 0; } /* ---------------- Floating text ---------------- */ var floaters = []; function floatText(x, y, text, color, big) { floaters.push({ x: x, y: y, text: text, color: color, life: 0.9, big: !!big }); if (floaters.length > 40) floaters.shift(); } function updateFloaters(dt) { for (var i = floaters.length - 1; i >= 0; i--) { var f = floaters[i]; f.y -= 34 * dt; f.life -= dt; if (f.life <= 0) floaters.splice(i, 1); } } function drawFloaters(ctx) { ctx.textAlign = 'center'; for (var i = 0; i < floaters.length; i++) { var f = floaters[i]; ctx.globalAlpha = Math.min(1, f.life * 2); ctx.font = (f.big ? 'bold 22px' : 'bold 13px') + ' "Segoe UI", system-ui, sans-serif'; ctx.fillStyle = f.color; ctx.fillText(f.text, f.x, f.y); } ctx.globalAlpha = 1; } function clearFloaters() { floaters.length = 0; } US.entities = { Player: Player, Enemy: Enemy, Bullet: Bullet, Pickup: Pickup, spawnParticles: spawnParticles, updateParticles: updateParticles, drawParticles: drawParticles, clearParticles: clearParticles, floatText: floatText, updateFloaters: updateFloaters, drawFloaters: drawFloaters, clearFloaters: clearFloaters }; })();