Primal Rampage — prehistoric survival wave shooter

Vanilla JS + Canvas 2D, zero dependencies.

Gameplay: campaign waves vs 12 dino species + 3 bosses, arena mode,
7 weapons, hero talents, pets, ult/bombs, card roguelite picks.
World: day/night cycle, weather fronts (rain+lightning), Blood Moon
weekends, 3 act biomes (jungle -> swamp -> ashlands with lava veins).
Meta: gems economy, upgrade tree, quests, bestiary, achievements,
shop (weapons/heroes/skins incl. premium), revive, ad-cap system,
endless scaling past wave 25, live-painted shop/bestiary icons,
pro main menu, auto-aim toggle (F). Headless smoke suite included.
This commit is contained in:
Primal Rampage Bot
2026-08-23 04:26:58 +00:00
commit 2024ea48b5
14 changed files with 7164 additions and 0 deletions
+442
View File
@@ -0,0 +1,442 @@
/* 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
};
})();