Files
Primal Rampage Bot 2024ea48b5 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.
2026-08-23 04:26:58 +00:00

1930 lines
64 KiB
JavaScript

/* PRIMAL RAMPAGE — procedural prehistoric art.
Every figure is drawn top-down with canvas paths: a tribal human hero,
dinosaurs with animated tails/jaws/wings/legs, and a jungle arena. */
(function () {
'use strict';
var US = (window.US = window.US || {});
var SKIN = '#c99862'; // base skin tone
var FUR = '#6e4a26'; // fur tunic
var FUR_D = '#553818';
var BONE = '#e8dcc4';
function rr(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function ell(ctx, x, y, rx, ry, rot) {
ctx.beginPath();
ctx.ellipse(x, y, rx, ry, rot || 0, 0, Math.PI * 2);
}
function tri(ctx, ax, ay, bx, by, cx2, cy2) {
ctx.beginPath();
ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.lineTo(cx2, cy2);
ctx.closePath();
}
function shadow(ctx, x, y, rx, ry) {
ctx.fillStyle = 'rgba(10,8,5,0.38)';
ell(ctx, x + rx * 0.25, y + ry * 0.45, rx, ry);
ctx.fill();
}
/* ================================================================
Arena ground & props
================================================================ */
function makeDecor(W, H, WALL) {
var d = { grass: [], rocks: [], bones: [], ferns: [], patches: [] };
var i;
for (i = 0; i < 16; i++) {
d.patches.push({
x: WALL + Math.random() * (W - WALL * 2),
y: WALL + Math.random() * (H - WALL * 2),
rx: 60 + Math.random() * 130,
ry: 40 + Math.random() * 80,
rot: Math.random() * Math.PI
});
}
for (i = 0; i < 90; i++) {
d.grass.push({
x: WALL + Math.random() * (W - WALL * 2),
y: WALL + Math.random() * (H - WALL * 2),
s: 0.7 + Math.random() * 0.9,
p: Math.random() * Math.PI * 2
});
}
for (i = 0; i < 14; i++) {
d.rocks.push({
x: WALL + 30 + Math.random() * (W - WALL * 2 - 60),
y: WALL + 30 + Math.random() * (H - WALL * 2 - 60),
r: 7 + Math.random() * 15,
pts: 5 + Math.floor(Math.random() * 3),
seed: Math.random() * 10
});
}
for (i = 0; i < 12; i++) {
d.bones.push({
x: WALL + Math.random() * (W - WALL * 2),
y: WALL + Math.random() * (H - WALL * 2),
rot: Math.random() * Math.PI,
skull: Math.random() < 0.35,
s: 0.8 + Math.random() * 0.7
});
}
for (i = 0; i < 18; i++) {
d.ferns.push({
x: WALL + Math.random() * (W - WALL * 2),
y: WALL + Math.random() * (H - WALL * 2),
s: 0.8 + Math.random() * 0.8,
rot: Math.random() * Math.PI * 2
});
}
// tar pits — sticky slow zones (kept clear of the central camp)
d.tarPits = [];
for (i = 0; i < 2; i++) {
var ang = i * Math.PI + 0.7;
d.tarPits.push({
x: W / 2 + Math.cos(ang) * (W * 0.31) + Math.random() * 40,
y: H / 2 + Math.sin(ang) * (H * 0.30) + Math.random() * 40,
r: 58 + Math.random() * 22,
seed: Math.random() * 9
});
}
// giant bone landmarks: a buried skull & a ribcage arch
d.bigProps = [
{ type: 'skull', x: WALL + 150, y: H - WALL - 130, s: 2.6, rot: -0.35 },
{ type: 'ribs', x: W - WALL - 170, y: WALL + 140, s: 2.4, rot: 0.25 }
];
// tribal totem stakes at the four inner corners
d.totems = [
{ x: WALL + 46, y: WALL + 46 },
{ x: W - WALL - 46, y: WALL + 46 },
{ x: WALL + 46, y: H - WALL - 46 },
{ x: W - WALL - 46, y: H - WALL - 46 }
];
return d;
}
function drawBiomeFx(ctx, g) {
var t = g.time || 0;
if (g.biome === 'swamp') {
// low creeping fog banks
for (var fi = 0; fi < 3; fi++) {
var fx2 = ((fi * 517 + t * (9 + fi * 5)) % (g.W + 700)) - 350;
var fy2 = g.H * (0.28 + fi * 0.22) + Math.sin(t * 0.23 + fi) * 30;
ctx.fillStyle = 'rgba(150,190,150,0.055)';
ell(ctx, fx2, fy2, 260 + fi * 55, 62 + fi * 14, fi * 0.7);
ctx.fill();
ell(ctx, fx2 + 170, fy2 + 26, 150 + fi * 40, 44, -fi * 0.5);
ctx.fill();
}
} else if (g.biome === 'ash' && g.decor && g.decor.cracks) {
// pulsing lava veins
var pulse = 0.55 + Math.abs(Math.sin(t * 2.1)) * 0.45;
ctx.lineCap = 'round';
for (var ci = 0; ci < g.decor.cracks.length; ci++) {
var cr = g.decor.cracks[ci];
ctx.strokeStyle = 'rgba(255,110,30,' + (0.75 * pulse).toFixed(2) + ')';
ctx.lineWidth = 5;
ctx.beginPath();
for (var si = 0; si < cr.pts.length; si++) {
var pt3 = cr.pts[si];
if (si === 0) ctx.moveTo(cr.x + pt3.x, cr.y + pt3.y);
else ctx.lineTo(cr.x + pt3.x, cr.y + pt3.y);
}
ctx.stroke();
ctx.strokeStyle = 'rgba(255,210,90,' + (0.5 * pulse).toFixed(2) + ')';
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.lineCap = 'butt';
}
}
/* ---------------- primeval landmarks -------------------------------- */
function drawTarPit(ctx, tp, t) {
ctx.fillStyle = '#151009';
ell(ctx, tp.x, tp.y, tp.r, tp.r * 0.72);
ctx.fill();
ell(ctx, tp.x + tp.r * 0.3, tp.y - tp.r * 0.15, tp.r * 0.55, tp.r * 0.4);
ctx.fill();
// oily sheen
ctx.fillStyle = 'rgba(90,70,45,0.20)';
ell(ctx, tp.x - tp.r * 0.2, tp.y - tp.r * 0.18, tp.r * 0.5, tp.r * 0.28, -0.3);
ctx.fill();
// slow bubbles
for (var b = 0; b < 3; b++) {
var cyc = (t * 0.35 + b / 3 + tp.seed) % 1;
var bx = tp.x + Math.sin(tp.seed * 7 + b * 2.4) * tp.r * 0.5;
var by = tp.y + Math.cos(tp.seed * 5 + b * 1.7) * tp.r * 0.3;
var br = (1 - cyc) * 4 + 1;
ctx.fillStyle = 'rgba(60,48,32,' + (cyc * 0.55).toFixed(2) + ')';
ell(ctx, bx, by, br, br * 0.8);
ctx.fill();
}
}
function drawGiantSkull(ctx, pr, t) {
shadow(ctx, pr.x + 10, pr.y + 14, 60 * pr.s * 0.5, 26 * pr.s * 0.5);
ctx.save();
ctx.translate(pr.x, pr.y);
ctx.rotate(pr.rot);
ctx.scale(pr.s, pr.s);
// cranium half-buried in the dirt
ctx.fillStyle = '#cfc2a4';
ctx.strokeStyle = '#8f8062';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-34, 6);
ctx.quadraticCurveTo(-36, -26, -8, -30);
ctx.quadraticCurveTo(24, -32, 34, -12);
ctx.lineTo(38, 14);
ctx.quadraticCurveTo(10, 22, -34, 6);
ctx.closePath();
ctx.fill();
ctx.stroke();
// dark eye sockets + nasal slit
ctx.fillStyle = '#171009';
ell(ctx, 8, -12, 11, 13, 0.15); ctx.fill();
ell(ctx, 30, -8, 8, 10, -0.2); ctx.fill();
tri(ctx, 18, 2, 23, 12, 28, 2); ctx.fill();
// upper teeth row along the jaw line
ctx.fillStyle = '#e8dcc4';
for (var th = 0; th < 6; th++) {
tri(ctx, -30 + th * 11, 10 + th * 0.8, -26 + th * 11, 22 + th * 0.6, -22 + th * 11, 10 + th * 1.1);
ctx.fill();
}
// cracks
ctx.strokeStyle = 'rgba(80,66,44,0.7)';
ctx.lineWidth = 1.4;
ctx.beginPath();
ctx.moveTo(-16, -26); ctx.lineTo(-10, -14); ctx.lineTo(-16, -4);
ctx.moveTo(14, -28); ctx.lineTo(18, -18);
ctx.stroke();
// grass tufts claiming it back
ctx.strokeStyle = '#46601f';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-30, 12); ctx.lineTo(-33, 4);
ctx.moveTo(-27, 13); ctx.lineTo(-28, 5);
ctx.stroke();
ctx.restore();
}
function drawRibcage(ctx, pr, t) {
shadow(ctx, pr.x, pr.y + 8, 52 * pr.s * 0.5, 14 * pr.s * 0.5);
ctx.save();
ctx.translate(pr.x, pr.y);
ctx.rotate(pr.rot);
ctx.scale(pr.s, pr.s);
// spine
ctx.fillStyle = '#cfc2a4';
rr(ctx, -50, -4, 104, 7, 3);
ctx.fill();
ctx.strokeStyle = '#8f8062';
ctx.lineWidth = 1.4;
ctx.stroke();
// curving ribs on both sides, weathered gaps
ctx.strokeStyle = '#d8ccb0';
ctx.lineWidth = 5.4;
ctx.lineCap = 'round';
for (var rb = 0; rb < 5; rb++) {
var rx = -42 + rb * 21;
ctx.beginPath();
ctx.moveTo(rx, 0);
ctx.quadraticCurveTo(rx - 4, 26, rx + 8, 44 - rb * 2);
ctx.stroke();
if (rb % 2 === 0) {
ctx.beginPath();
ctx.moveTo(rx, 0);
ctx.quadraticCurveTo(rx + 2, -18, rx - 6, -30 + rb * 2);
ctx.stroke();
}
}
ctx.lineCap = 'butt';
ctx.restore();
}
function drawTotem(ctx, tt, t) {
shadow(ctx, tt.x + 5, tt.y + 8, 12, 7);
// carved pole
ctx.fillStyle = '#4a3620';
ctx.fillRect(tt.x - 7, tt.y - 34, 14, 46);
ctx.strokeStyle = '#2b1d0d';
ctx.lineWidth = 1.6;
ctx.strokeRect(tt.x - 7, tt.y - 34, 14, 46);
// stacked carved faces
ctx.fillStyle = '#6b4a26';
ell(ctx, tt.x, tt.y - 24, 6.5, 6);
ctx.fill();
ctx.fillStyle = '#171009';
ell(ctx, tt.x - 2.6, tt.y - 25, 1.5, 1.8); ctx.fill();
ell(ctx, tt.x + 2.6, tt.y - 25, 1.5, 1.8); ctx.fill();
ctx.fillStyle = '#6b4a26';
ell(ctx, tt.x, tt.y - 8, 6, 5.4);
ctx.fill();
ctx.strokeStyle = BONE;
ctx.lineWidth = 1.4;
ctx.beginPath(); ctx.moveTo(tt.x - 4, tt.y - 6); ctx.lineTo(tt.x + 4, tt.y - 6); ctx.stroke();
// small flame on top, alive
var fh = 9 + Math.sin(t * 11 + tt.x) * 2.4;
ctx.fillStyle = '#e04b3a';
tri(ctx, tt.x - 4.5, tt.y - 34, tt.x, tt.y - 34 - fh, tt.x + 4.5, tt.y - 34);
ctx.fill();
ctx.fillStyle = '#ffb938';
tri(ctx, tt.x - 2.4, tt.y - 34, tt.x, tt.y - 34 - fh * 0.62, tt.x + 2.4, tt.y - 34);
ctx.fill();
}
/* ---------------- the tribe's campfire (world centre) --------------- */
function drawCampfire(ctx, cx, cy, t) {
// flickering warm light pool
var fl = 0.12 + Math.abs(Math.sin(t * 7.3)) * 0.05 + Math.abs(Math.sin(t * 3.1)) * 0.03;
var g1 = ctx.createRadialGradient(cx, cy, 8, cx, cy, 130);
g1.addColorStop(0, 'rgba(255,150,60,' + fl.toFixed(3) + ')');
g1.addColorStop(1, 'rgba(255,120,40,0)');
ctx.fillStyle = g1;
ctx.fillRect(cx - 130, cy - 130, 260, 260);
// stone ring
ctx.fillStyle = '#6a6156';
for (var st2 = 0; st2 < 9; st2++) {
var sa2 = st2 / 9 * Math.PI * 2;
ell(ctx, cx + Math.cos(sa2) * 26, cy + Math.sin(sa2) * 17, 5.5, 4, sa2);
ctx.fill();
}
// crossed logs
ctx.save();
ctx.translate(cx, cy);
ctx.fillStyle = '#5c4423';
ctx.rotate(0.5); rr(ctx, -16, -3.4, 32, 6.8, 3); ctx.fill();
ctx.rotate(-1.05); rr(ctx, -16, -3.4, 32, 6.8, 3); ctx.fill();
ctx.restore();
// layered flames
for (var f2 = 0; f2 < 3; f2++) {
var jx = Math.sin(t * 13 + f2 * 2.2) * 3;
var hh = [22, 15, 9][f2];
var cols = ['#c22f1d', '#ff8a3d', '#ffd24a'];
ctx.fillStyle = cols[f2];
ctx.beginPath();
ctx.moveTo(cx - (9 - f2 * 2.4), cy - 2);
ctx.quadraticCurveTo(cx - (5 - f2), cy - 2 - hh * 0.62, cx + jx, cy - 2 - hh);
ctx.quadraticCurveTo(cx + (5 - f2), cy - 2 - hh * 0.62, cx + (9 - f2 * 2.4), cy - 2);
ctx.closePath();
ctx.fill();
}
// rising sparks
for (var sp3 = 0; sp3 < 3; sp3++) {
var cyc2 = (t * 0.9 + sp3 / 3) % 1;
ctx.globalAlpha = (1 - cyc2) * 0.85;
ctx.fillStyle = sp3 % 2 ? '#ffb938' : '#ff8a3d';
ell(ctx,
cx + Math.sin(t * 3 + sp3 * 5) * (4 + sp3 * 3),
cy - 10 - cyc2 * 34, 1.7, 1.7, 0);
ctx.fill();
}
ctx.globalAlpha = 1;
}
/* ---------------- biomes: jungle -> swamp -> ashlands ----------------- */
var BIOMES = {
jungle: { inner: '#3d2f1d', outer: '#191207', patch: 'rgba(120,92,54,0.13)',
grass: ['#46601f', '#58a55c', '#6f8f3a'], rim: 'rgba(190,160,105,0.16)' },
swamp: { inner: '#22301c', outer: '#0e1509', patch: 'rgba(70,110,70,0.14)',
grass: ['#2f4a22', '#3c5a2a', '#4f7a35'], rim: 'rgba(140,180,130,0.13)' },
ash: { inner: '#33221a', outer: '#120b08', patch: 'rgba(150,60,25,0.10)',
grass: ['#4a3428', '#5c4030', '#3a2a20'], rim: 'rgba(255,140,70,0.14)' }
};
function drawGround(g, ctx) {
var tNow = g.time || performance.now() / 1000;
var PAL = BIOMES[g.biome] || BIOMES.jungle;
// packed-dirt base with dusk vignette, tinted by biome
var grad = ctx.createRadialGradient(g.W / 2, g.H / 2, g.H * 0.25, g.W / 2, g.H / 2, g.H * 0.85);
grad.addColorStop(0, PAL.inner);
grad.addColorStop(1, PAL.outer);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, g.W, g.H);
if (!g.decor) return;
var d = g.decor, i, j;
// lighter trampled patches (biome-tinted)
ctx.fillStyle = PAL.patch;
for (i = 0; i < d.patches.length; i++) {
var pa = d.patches[i];
ell(ctx, pa.x, pa.y, pa.rx, pa.ry, pa.rot);
ctx.fill();
}
// home-turf circle at center
ctx.strokeStyle = PAL.rim;
ctx.lineWidth = 10;
ell(ctx, g.W / 2, g.H / 2, 130, 84);
ctx.stroke();
// grass tufts (biome palette)
ctx.lineWidth = 2;
for (i = 0; i < d.grass.length; i++) {
var gr = d.grass[i];
ctx.strokeStyle = PAL.grass[i % 3];
var sway = Math.sin(gr.p + performance.now() / 900) * 2 * gr.s;
for (j = -1; j <= 1; j++) {
ctx.beginPath();
ctx.moveTo(gr.x, gr.y);
ctx.quadraticCurveTo(gr.x + j * 3 * gr.s, gr.y - 6 * gr.s, gr.x + j * 4 * gr.s + sway, gr.y - 11 * gr.s);
ctx.stroke();
}
}
// ferns
for (i = 0; i < d.ferns.length; i++) {
var f = d.ferns[i];
ctx.save();
ctx.translate(f.x, f.y);
ctx.rotate(f.rot);
ctx.scale(f.s, f.s);
ctx.strokeStyle = '#37531f';
for (var k = -2; k <= 2; k++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(k * 5, -9, k * 9, -17);
ctx.stroke();
}
ctx.restore();
}
// rocks
for (i = 0; i < d.rocks.length; i++) {
var rk = d.rocks[i];
shadow(ctx, rk.x, rk.y, rk.r, rk.r * 0.6);
ctx.fillStyle = '#6a6156';
ctx.beginPath();
for (j = 0; j < rk.pts; j++) {
var a = j / rk.pts * Math.PI * 2 + rk.seed;
var rad = rk.r * (0.75 + ((Math.sin(rk.seed * 9 + j * 3) + 1) / 2) * 0.4);
ctx[j ? 'lineTo' : 'moveTo'](rk.x + Math.cos(a) * rad, rk.y + Math.sin(a) * rad);
}
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(255,240,210,0.14)';
ell(ctx, rk.x - rk.r * 0.25, rk.y - rk.r * 0.3, rk.r * 0.4, rk.r * 0.22, -0.5);
ctx.fill();
}
// old bones
for (i = 0; i < d.bones.length; i++) {
var b = d.bones[i];
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.rot);
ctx.scale(b.s, b.s);
ctx.fillStyle = BONE;
ctx.globalAlpha = 0.85;
if (b.skull) {
ell(ctx, 0, 0, 8, 6.5);
ctx.fill();
ctx.fillStyle = '#241a10';
ell(ctx, -3, -2, 1.7, 2.2); ctx.fill();
ell(ctx, 3, -2, 1.7, 2.2); ctx.fill();
ctx.fillRect(-3, 4, 6, 2.4);
} else {
ell(ctx, -8, 0, 4.5, 3.2); ctx.fill();
ell(ctx, 8, 0, 4.5, 3.2); ctx.fill();
ctx.fillRect(-7, -1.6, 14, 3.2);
}
ctx.restore();
}
ctx.globalAlpha = 1;
// tar pits (slow zones)
if (d.tarPits) for (i = 0; i < d.tarPits.length; i++) drawTarPit(ctx, d.tarPits[i], tNow);
// giant bone landmarks
if (d.bigProps) for (i = 0; i < d.bigProps.length; i++) {
var bp = d.bigProps[i];
if (bp.type === 'skull') drawGiantSkull(ctx, bp, tNow);
else drawRibcage(ctx, bp, tNow);
}
// tribal totem stakes
if (d.totems) for (i = 0; i < d.totems.length; i++) drawTotem(ctx, d.totems[i], tNow);
// the tribe's campfire at the heart of the arena
drawCampfire(ctx, g.W / 2, g.H / 2, tNow);
// palisade wall of logs around the arena
var stepL = 15;
var n = 0;
for (var x = g.WALL; x <= g.W - g.WALL; x += stepL, n++) {
logPost(ctx, x, g.WALL, n);
logPost(ctx, x, g.H - g.WALL - 12, n + 37);
}
for (var y = g.WALL + stepL; y < g.H - g.WALL; y += stepL, n++) {
logPostV(ctx, g.WALL, y, n + 71);
logPostV(ctx, g.W - g.WALL - 12, y, n + 113);
}
}
function logPost(ctx, x, y, n) {
var h = 13 + (n % 3);
ctx.fillStyle = '#463319';
ctx.fillRect(x, y, 9, h);
ctx.fillStyle = '#5c4423';
ctx.fillRect(x, y, 9, 3);
ell(ctx, x + 4.5, y + 1.5, 4, 2.2);
ctx.fillStyle = '#7a5c33';
ctx.fill();
}
function logPostV(ctx, x, y, n) {
var h = 13 + (n % 3);
ctx.fillStyle = '#463319';
ctx.fillRect(x, y, h, 9);
ctx.fillStyle = '#5c4423';
ctx.fillRect(x, y, 3, 9);
}
/* ================================================================
The tribal hero (player & rival tribesmen)
Top-down but chunky & cel-outlined so it reads instantly:
planted striding feet, fur tunic, war-paint sash, two-segment arms
gripping a real weapon, hair bun + bone pin, nose showing facing,
recoil + muzzle flash while firing, idle breathing.
================================================================ */
var OUT = '#241708';
// u: {x,y,radius,aim,walkPhase,moving,color(accent paint),trim,
// weaponKind,label,hurtT,fireT,scale,seedF}
function drawHuman(ctx, u, t) {
var moving = !!u.moving;
var phase = u.walkPhase || 0;
var stepA = moving ? Math.sin(phase) : 0;
var s = u.scale || 1;
var fire = u.fireT > 0 ? Math.min(1, u.fireT / 0.09) : 0;
shadow(ctx, u.x, u.y, u.radius * 1.25 * s, u.radius * 0.85 * s);
ctx.save();
ctx.translate(u.x, u.y);
ctx.scale(s, s);
ctx.rotate(u.aim || 0);
// ---- planted feet (under the tunic hem, alternate stride) ----
ctx.fillStyle = '#4b3218';
ctx.strokeStyle = OUT;
ctx.lineWidth = 1.6;
var fy = moving ? 8 : 7;
ell(ctx, -3 + stepA * 6, -fy, 4.8, 3, 0.25);
ctx.fill(); ctx.stroke();
ell(ctx, -3 - stepA * 6, fy, 4.8, 3, -0.25);
ctx.fill(); ctx.stroke();
// ---- everything above the feet recoils when firing ----
ctx.save();
ctx.translate(-fire * 2.6, 0);
// breathing / run bob
var pulse = moving ? Math.abs(Math.sin(phase)) * 0.045 : Math.sin(t * 2.3 + (u.seedF || 0)) * 0.02;
ctx.scale(1 + pulse, 1 - pulse * 0.6);
// tunic dyed in the tribe's colours (falls back to plain fur)
var OUTFIT = u.body || FUR;
ctx.fillStyle = OUTFIT;
rr(ctx, -11, -10.5, 20, 21, 9);
ctx.fill();
ctx.strokeStyle = OUT;
ctx.lineWidth = 2;
ctx.stroke();
// lighter lap band + texture notches
ctx.fillStyle = 'rgba(255,235,200,0.18)';
rr(ctx, -8.5, -8, 15, 16, 7);
ctx.fill();
ctx.strokeStyle = 'rgba(20,12,4,0.4)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-7, -6); ctx.lineTo(-3, -3);
ctx.moveTo(-8, 1); ctx.lineTo(-4, 4);
ctx.moveTo(-7, 8); ctx.lineTo(-3, 5);
ctx.stroke();
// shoulder tufts shaded to match the dye
ctx.fillStyle = shade(OUTFIT, -28);
ctx.strokeStyle = OUT;
ctx.lineWidth = 1.4;
ell(ctx, -4, -9.5, 3.4, 2.6, -0.4); ctx.fill(); ctx.stroke();
ell(ctx, -4, 9.5, 3.4, 2.6, 0.4); ctx.fill(); ctx.stroke();
// war-paint sash across the chest
ctx.strokeStyle = OUT;
ctx.lineWidth = 6.4;
ctx.beginPath(); ctx.moveTo(-4.5, -10); ctx.quadraticCurveTo(3, 0, -4.5, 10); ctx.stroke();
ctx.strokeStyle = u.color || '#e04b3a';
ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(-4.5, -9.6); ctx.quadraticCurveTo(3, 0, -4.5, 9.6); ctx.stroke();
// ---- arms: dark under-line then skin => cartoon outline ----
var handX = 14 + fire * 3;
function arm(sy, hy) {
ctx.lineCap = 'round';
ctx.strokeStyle = OUT; ctx.lineWidth = 6.6;
ctx.beginPath();
ctx.moveTo(0, sy); ctx.quadraticCurveTo(7, sy * 0.85, handX, hy);
ctx.stroke();
ctx.strokeStyle = SKIN; ctx.lineWidth = 4.2;
ctx.beginPath();
ctx.moveTo(0, sy); ctx.quadraticCurveTo(7, sy * 0.85, handX, hy);
ctx.stroke();
}
arm(-8.5, -3);
arm(8.5, 3);
// ---- weapon in the grip (returns tip x for muzzle flash) ----
var tipX = drawHeldWeapon(ctx, u.weaponKind, fire);
// ---- night torch: the reason this hunter carries a light halo ----
if (u.torch) {
ctx.save();
ctx.translate(handX - 2, 4);
ctx.rotate(-0.5);
// shaft
st(ctx, OUT, 6.2); line(ctx, 0, 6, 0, -8);
st(ctx, '#7a5230', 3.8); line(ctx, 0, 6, 0, -8);
// wrapped head
ctx.fillStyle = '#c9a15a';
ell(ctx, 0, -10, 3.4, 3, 0); ctx.fill();
ctx.strokeStyle = OUT; ctx.lineWidth = 1.4;
ctx.stroke();
// layered flame
var fh2 = 11 + Math.sin(t * 12 + (u.seedF || 0)) * 2.6;
ctx.fillStyle = '#c22f1d';
tri(ctx, -4, -12, 0, -12 - fh2, 4, -12); ctx.fill();
ctx.fillStyle = '#ff8a3d';
tri(ctx, -2.6, -12, 0, -12 - fh2 * 0.66, 2.6, -12); ctx.fill();
ctx.fillStyle = '#ffd24a';
tri(ctx, -1.4, -12, 0, -12 - fh2 * 0.38, 1.4, -12); ctx.fill();
// stray spark
if ((t * 2 + (u.seedF || 0)) % 1 < 0.5) {
ctx.fillStyle = '#ffb938';
ell(ctx, Math.sin(t * 7) * 3, -14 - fh2 * 0.9, 1.5, 1.5, 0);
ctx.fill();
}
ctx.restore();
}
// ---- head stack ----
// hair mass
ctx.fillStyle = '#33240f';
ctx.strokeStyle = OUT;
ctx.lineWidth = 1.8;
ell(ctx, -1.5, 0, 7.6, 7.9, 0);
ctx.fill(); ctx.stroke();
// top-knot bun + bone pin
ctx.fillStyle = '#33240f';
ell(ctx, -6.5, 0, 3.4, 3.4, 0);
ctx.fill(); ctx.stroke();
ctx.strokeStyle = BONE;
ctx.lineWidth = 1.7;
ctx.beginPath();
ctx.moveTo(-10.5, -2.6); ctx.lineTo(-3.2, 2.6);
ctx.stroke();
// two feathers crossing behind the head
feather(ctx, Math.PI - 0.30, u.color || '#e04b3a', t, u.seedF || 0, 15);
feather(ctx, Math.PI + 0.24, '#e8dcc4', t, (u.seedF || 0) + 2.1, 12);
// face/crown
ctx.fillStyle = SKIN;
ell(ctx, 2.8, 0, 6.4, 6.8, 0);
ctx.fill();
ctx.strokeStyle = OUT;
ctx.lineWidth = 1.8;
ctx.stroke();
// nose bump points where you are looking
ctx.fillStyle = SKIN;
tri(ctx, 7.6, -2.2, 10.8, 0, 7.6, 2.2);
ctx.fill();
ctx.strokeStyle = OUT;
ctx.stroke();
// headband
ctx.strokeStyle = OUT;
ctx.lineWidth = 4.6;
ctx.beginPath(); ctx.arc(0.6, 0, 6.6, -Math.PI * 0.42, Math.PI * 0.42); ctx.stroke();
ctx.strokeStyle = u.color || '#e04b3a';
ctx.lineWidth = 2.8;
ctx.beginPath(); ctx.arc(0.6, 0, 6.6, -Math.PI * 0.40, Math.PI * 0.40); ctx.stroke();
ctx.restore(); // recoil/bob group
// ---- muzzle flash at the weapon tip ----
if (fire > 0) {
ctx.globalAlpha = fire;
var mx2 = tipX + 3;
ctx.fillStyle = 'rgba(255,185,56,0.9)';
for (var spk = 0; spk < 4; spk++) {
var sa = (spk / 4) * Math.PI * 2 + t * 30;
tri(ctx, mx2, 0,
mx2 + Math.cos(sa) * (7 + fire * 5), Math.sin(sa) * (7 + fire * 5),
mx2 + Math.cos(sa + 0.5) * 2, Math.sin(sa + 0.5) * 2);
ctx.fill();
}
ctx.fillStyle = '#fff3c4';
ell(ctx, mx2 + 1, 0, 3.2 + fire * 2, 2.6 + fire * 1.6, 0);
ctx.fill();
ctx.globalAlpha = 1;
}
ctx.restore(); // aim/position
// hurt flash
if (u.hurtT > 0) {
ctx.fillStyle = 'rgba(224,75,58,' + Math.min(0.5, u.hurtT) + ')';
ctx.beginPath();
ctx.ellipse(u.x, u.y, u.radius + 5, u.radius + 3, 0, 0, Math.PI * 2);
ctx.fill();
}
// name tag for rival tribesmen
if (u.label) {
ctx.fillStyle = '#f3e7cf';
ctx.font = 'bold 10px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(u.label, u.x, u.y - u.radius - 13);
}
}
function feather(ctx, baseAng, col, t, seed, len) {
ctx.save();
ctx.rotate(baseAng + Math.sin(t * 6 + seed) * 0.12);
ctx.fillStyle = col;
ctx.strokeStyle = OUT;
ctx.lineWidth = 1.4;
ctx.beginPath();
ctx.moveTo(2, 0);
ctx.quadraticCurveTo(len * 0.55, -3.4, len, -1.2);
ctx.quadraticCurveTo(len * 0.55, 2.6, 2, 0);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.strokeStyle = 'rgba(36,23,8,0.55)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(3, 0); ctx.lineTo(len - 2, -0.6);
ctx.stroke();
ctx.restore();
}
// draws the held weapon pointing along +x; returns the muzzle tip x
function drawHeldWeapon(ctx, kind, fire) {
var tip = 30;
switch (kind) {
case 'slingshot':
st(ctx, '#7a5230', 5.6); line(ctx, 10, 0, 20, 0); prong(ctx, 20, 0, 26, -5.5); prong(ctx, 20, 0, 26, 5.5);
st(ctx, '#8f6636', 3.2); line(ctx, 10, 0, 20, 0); prong(ctx, 20, 0, 26, -5.5); prong(ctx, 20, 0, 26, 5.5);
st(ctx, 'rgba(240,230,210,0.9)', 1.5);
ctx.beginPath(); ctx.moveTo(26, -5.5); ctx.quadraticCurveTo(29 - fire * 3, 0, 26, 5.5); ctx.stroke();
ctx.fillStyle = stoneCol;
ell(ctx, 29 - fire * 3, 0, 2.4, 2.4, 0); ctx.fill();
tip = 31;
break;
case 'blowpipe':
st(ctx, OUT, 5.4); line(ctx, 5, 3, 28, 3);
st(ctx, '#8a6234', 3.2); line(ctx, 5, 3, 28, 3);
st(ctx, '#5c4423', 3.2); line(ctx, 5, 3, 8, 3);
ctx.fillStyle = BONE; ell(ctx, 28.5, 3, 2, 2, 0); ctx.fill();
tip = 30;
break;
case 'spears':
st(ctx, OUT, 5.6); line(ctx, 2, 4, 27, 4);
st(ctx, '#8a6234', 3.4); line(ctx, 2, 4, 27, 4);
st(ctx, '#5c4423', 3.4); line(ctx, 12, 4, 15, 4); line(ctx, 20, 4, 23, 4);
ctx.fillStyle = stoneCol;
ctx.strokeStyle = OUT; ctx.lineWidth = 1.5;
tri(ctx, 27, 0.6, 36, 4, 27, 7.4);
ctx.fill(); ctx.stroke();
tip = 37;
break;
case 'obelisk':
st(ctx, OUT, 5.6); line(ctx, 2, 4, 23, 4);
st(ctx, '#6b4a26', 3.4); line(ctx, 2, 4, 23, 4);
ctx.fillStyle = '#4b4457';
ctx.strokeStyle = OUT; ctx.lineWidth = 1.5;
tri(ctx, 23, -1, 39, 4, 23, 9);
ctx.fill(); ctx.stroke();
ctx.strokeStyle = 'rgba(199,146,255,0.75)';
ctx.lineWidth = 1.4;
ctx.beginPath(); ctx.moveTo(26, 3.4); ctx.lineTo(35, 4); ctx.stroke();
tip = 40;
break;
case 'trebuchet':
default:
st(ctx, OUT, 5.8); line(ctx, 4, 4, 18, 4);
st(ctx, '#7a5230', 3.6); line(ctx, 4, 4, 18, 4);
ctx.fillStyle = '#6a6156';
ctx.strokeStyle = OUT; ctx.lineWidth = 1.7;
ell(ctx, 24, 4, 6.6, 5.6, 0.2);
ctx.fill(); ctx.stroke();
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
ctx.lineWidth = 1.3;
ctx.beginPath(); ctx.moveTo(21, 1.6); ctx.lineTo(25, 4.6); ctx.stroke();
tip = 31;
}
ctx.lineCap = 'butt';
return tip;
}
function st(ctx, c, w) { ctx.strokeStyle = c; ctx.lineWidth = w; ctx.lineCap = 'round'; }
function line(ctx, x0, y0, x1, y1) { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); }
function prong(ctx, x0, y0, x1, y1) { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke(); }
var stoneCol = '#8f8578';
/* ================================================================
Dinosaurs — shared bits
================================================================ */
function legsPair(ctx, off, phase, len, wid, col, dark) {
var s = Math.sin(phase) * len * 0.35;
ctx.fillStyle = dark;
ell(ctx, off + s, -len * 0.42, wid, len * 0.32, 0.3);
ctx.fill();
ell(ctx, off - s, len * 0.42, wid, len * 0.32, -0.3);
ctx.fill();
}
function taperTail(ctx, x0, y0, len, wid, sway, col) {
ctx.fillStyle = col;
ctx.beginPath();
ctx.moveTo(x0, y0 - wid);
ctx.quadraticCurveTo(x0 - len * 0.5, y0 - wid * 0.7 + sway * 0.4, x0 - len, y0 + sway);
ctx.quadraticCurveTo(x0 - len * 0.5, y0 + wid * 0.7 + sway * 0.4, x0, y0 + wid);
ctx.closePath();
ctx.fill();
}
function hpSliver(ctx, e, col) {
if (e.hp >= e.maxHp) return;
var r = e.radius;
ctx.fillStyle = 'rgba(20,14,8,0.72)';
ctx.fillRect(e.x - r, e.y - r - 10, r * 2, 4.4);
ctx.fillStyle = col || '#e04b3a';
ctx.fillRect(e.x - r, e.y - r - 10, r * 2 * Math.max(0, e.hp / e.maxHp), 4.4);
}
/* ---------------- Raptors (grunt / compy / alpha / hatchling) ------- */
function drawRaptor(ctx, e, t, opts) {
opts = opts || {};
var r = e.radius;
var col = e.hitFlash > 0 ? '#fff6ea' : e.color;
var dark = shade(col, -30);
var ph = t * 11 + (e.seed || 0) * 7;
var sway = Math.sin(t * 7 + (e.seed || 0) * 5) * r * 0.35;
shadow(ctx, e.x, e.y, r * 1.35, r * 0.8);
ctx.save();
ctx.translate(e.x, e.y);
ctx.rotate(e.face || 0);
taperTail(ctx, -r * 0.7, 0, r * 2.1, r * 0.42, sway, dark);
legsPair(ctx, -r * 0.25, ph, r * 1.05, r * 0.34, col, dark);
// body
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.08, r * 0.68);
ctx.fill();
// back stripes
ctx.strokeStyle = 'rgba(30,20,8,0.5)';
ctx.lineWidth = 2;
for (var i = -1; i <= 1; i++) {
ctx.beginPath();
ctx.arc(i * r * 0.5, 0, r * 0.5, Math.PI * 0.65, Math.PI * 1.35);
ctx.stroke();
}
// belly highlight
ctx.fillStyle = 'rgba(255,235,200,0.18)';
ell(ctx, r * 0.05, 0, r * 0.7, r * 0.34);
ctx.fill();
// little clawed arms
ctx.strokeStyle = dark;
ctx.lineWidth = 2.4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(r * 0.55, -r * 0.4); ctx.lineTo(r * 0.95, -r * 0.62);
ctx.moveTo(r * 0.55, r * 0.4); ctx.lineTo(r * 0.95, r * 0.62);
ctx.stroke();
// neck + head with animated jaw
var jaw = 0.25 + 0.2 * Math.sin(t * 6 + (e.seed || 0));
ctx.fillStyle = col;
ell(ctx, r * 0.95, 0, r * 0.42, r * 0.34);
ctx.fill();
ctx.save();
ctx.translate(r * 1.15, 0);
// upper snout
ctx.fillStyle = col;
tri(ctx, -r * 0.15, -r * 0.3, r * 0.75, -r * 0.06, -r * 0.15, r * 0.06);
ctx.fill();
// lower jaw drops
ctx.save();
ctx.rotate(jaw);
ctx.fillStyle = dark;
tri(ctx, -r * 0.1, r * 0.02, r * 0.62, r * 0.3, -r * 0.1, r * 0.22);
ctx.fill();
ctx.restore();
// teeth glint + eye
ctx.fillStyle = BONE;
ctx.fillRect(r * 0.3, r * 0.02, r * 0.3, 1.4);
ctx.fillStyle = opts.alpha ? '#ff5040' : '#1d1408';
ell(ctx, -r * 0.02, -r * 0.16, 1.6, 1.6);
ctx.fill();
ctx.restore();
if (opts.alpha) {
// feather mane
ctx.strokeStyle = '#c9432f';
ctx.lineWidth = 2;
for (var m = -2; m <= 2; m++) {
ctx.beginPath();
ctx.moveTo(r * 0.55, m * r * 0.16);
ctx.lineTo(r * 0.78, m * r * 0.34);
ctx.stroke();
}
}
ctx.lineCap = 'butt';
ctx.restore();
hpSliver(ctx, e);
}
/* ---------------- Triceratops (tank) -------------------------------- */
function drawTriceratops(ctx, e, t) {
var r = e.radius;
var col = e.hitFlash > 0 ? '#fff6ea' : e.color;
var dark = shade(col, -28);
var ph = t * 7 + (e.seed || 0) * 6;
shadow(ctx, e.x, e.y, r * 1.3, r * 0.95);
ctx.save();
ctx.translate(e.x, e.y);
ctx.rotate(e.face || 0);
taperTail(ctx, -r * 0.8, 0, r * 1.1, r * 0.5, Math.sin(t * 5 + e.seed) * r * 0.2, dark);
// four stocky legs
ctx.fillStyle = dark;
ell(ctx, -r * 0.45 + Math.sin(ph) * 3, -r * 0.62, r * 0.3, r * 0.22, 0.2);
ctx.fill();
ell(ctx, -r * 0.45 - Math.sin(ph) * 3, r * 0.62, r * 0.3, r * 0.22, -0.2);
ctx.fill();
ell(ctx, r * 0.4 - Math.sin(ph) * 3, -r * 0.6, r * 0.3, r * 0.22, 0.2);
ctx.fill();
ell(ctx, r * 0.4 + Math.sin(ph) * 3, r * 0.6, r * 0.3, r * 0.22, -0.2);
ctx.fill();
// barrel body
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.05, r * 0.88);
ctx.fill();
ctx.fillStyle = 'rgba(255,235,200,0.15)';
ell(ctx, -r * 0.1, 0, r * 0.75, r * 0.5);
ctx.fill();
// bony frill behind the head
ctx.fillStyle = dark;
ctx.beginPath();
ctx.arc(r * 0.55, 0, r * 0.72, -Math.PI * 0.62, Math.PI * 0.62);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = shade(col, -45);
ctx.lineWidth = 2;
for (var i = -2; i <= 2; i++) {
var aa = i * 0.42;
ctx.beginPath();
ctx.moveTo(r * 0.55 + Math.cos(aa) * r * 0.45, Math.sin(aa) * r * 0.45);
ctx.lineTo(r * 0.55 + Math.cos(aa) * r * 0.68, Math.sin(aa) * r * 0.68);
ctx.stroke();
}
// head, beak, three horns
ctx.fillStyle = col;
ell(ctx, r * 1.05, 0, r * 0.4, r * 0.34);
ctx.fill();
tri(ctx, r * 1.2, -r * 0.18, r * 1.62, 0, r * 1.2, r * 0.18);
ctx.fill();
ctx.strokeStyle = BONE;
ctx.lineCap = 'round';
ctx.lineWidth = 3.2;
ctx.beginPath();
ctx.moveTo(r * 1.0, -r * 0.3); ctx.quadraticCurveTo(r * 1.45, -r * 0.52, r * 1.72, -r * 0.42);
ctx.moveTo(r * 1.0, r * 0.3); ctx.quadraticCurveTo(r * 1.45, r * 0.52, r * 1.72, r * 0.42);
ctx.stroke();
ctx.lineWidth = 2.6;
ctx.beginPath();
ctx.moveTo(r * 1.32, 0); ctx.lineTo(r * 1.56, 0);
ctx.stroke();
ctx.lineCap = 'butt';
// eye
ctx.fillStyle = '#1d1408';
ell(ctx, r * 0.86, -r * 0.2, 1.7, 1.7);
ctx.fill();
ctx.restore();
hpSliver(ctx, e);
}
/* ---------------- Dilophosaurus (spitter) --------------------------- */
function drawDilo(ctx, e, t) {
var r = e.radius;
var col = e.hitFlash > 0 ? '#fff6ea' : e.color;
var dark = shade(col, -30);
var ph = t * 10 + (e.seed || 0) * 8;
var aboutToSpit = e.def && e.def.ranged && e.shootCd < 0.45 && e.shootCd > 0;
shadow(ctx, e.x, e.y, r * 1.25, r * 0.75);
ctx.save();
ctx.translate(e.x, e.y);
ctx.rotate(e.face || 0);
taperTail(ctx, -r * 0.7, 0, r * 1.9, r * 0.38, Math.sin(t * 7 + e.seed) * r * 0.4, dark);
legsPair(ctx, -r * 0.2, ph, r, r * 0.3, col, dark);
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.05, r * 0.6);
ctx.fill();
ctx.fillStyle = 'rgba(255,240,180,0.2)';
ell(ctx, r * 0.05, 0, r * 0.68, r * 0.28);
ctx.fill();
// neck & head
ell(ctx, r * 0.9, 0, r * 0.38, r * 0.3);
ctx.fillStyle = col;
ctx.fill();
// threat frill flares right before spitting
if (aboutToSpit) {
ctx.fillStyle = 'rgba(224,75,58,0.82)';
ctx.beginPath();
ctx.arc(r * 0.95, 0, r * 1.15, -Math.PI * 0.55, Math.PI * 0.55);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#8f2c1c';
ctx.lineWidth = 1.6;
for (var s = -3; s <= 3; s++) {
var ra = s * 0.26;
ctx.beginPath();
ctx.moveTo(r * 0.95 + Math.cos(ra) * r * 0.4, Math.sin(ra) * r * 0.4);
ctx.lineTo(r * 0.95 + Math.cos(ra) * r * 1.1, Math.sin(ra) * r * 1.1);
ctx.stroke();
}
}
// double head-crests
ctx.fillStyle = '#e04b3a';
ell(ctx, r * 1.02, -r * 0.3, r * 0.34, r * 0.16, -0.35);
ctx.fill();
ell(ctx, r * 1.02, r * 0.3, r * 0.34, r * 0.16, 0.35);
ctx.fill();
// slim snout
ctx.fillStyle = col;
tri(ctx, r * 1.05, -r * 0.16, r * 1.7, 0, r * 1.05, r * 0.16);
ctx.fill();
ctx.fillStyle = '#1d1408';
ell(ctx, r * 1.06, -r * 0.12, 1.5, 1.5);
ctx.fill();
ctx.restore();
hpSliver(ctx, e);
}
/* ---------------- Pteranodon (splitter) ----------------------------- */
function drawPtero(ctx, e, t, chick) {
var r = e.radius;
var col = e.hitFlash > 0 ? '#fff6ea' : e.color;
var dark = shade(col, -30);
var flap = Math.sin(t * 9 + (e.seed || 0) * 6) * 0.38;
// flying: smaller offset shadow
ctx.fillStyle = 'rgba(10,8,5,0.25)';
ell(ctx, e.x + 10, e.y + 14, r * 0.9, r * 0.45);
ctx.fill();
ctx.save();
ctx.translate(e.x, e.y);
ctx.rotate(e.face || 0);
// wings
ctx.fillStyle = col;
for (var side = -1; side <= 1; side += 2) {
ctx.save();
ctx.rotate(side * flap);
ctx.beginPath();
ctx.moveTo(r * 0.2, side * r * 0.2);
ctx.quadraticCurveTo(-r * 0.4, side * r * 1.5, -r * 0.9, side * r * 1.9);
ctx.quadraticCurveTo(-r * 0.15, side * r * 1.15, r * 0.35, side * r * 0.55);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = dark;
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.moveTo(r * 0.2, side * r * 0.25);
ctx.lineTo(-r * 0.8, side * r * 1.75);
ctx.stroke();
ctx.restore();
}
// body + long tail-less torso
ctx.fillStyle = dark;
ell(ctx, -r * 0.15, 0, r * 0.75, r * 0.3);
ctx.fill();
// head with backward crest and forward beak
ctx.fillStyle = col;
ell(ctx, r * 0.55, 0, r * 0.3, r * 0.24);
ctx.fill();
tri(ctx, r * 0.6, -r * 0.1, r * 1.5, 0, r * 0.6, r * 0.1);
ctx.fill();
ctx.fillStyle = '#e04b3a';
tri(ctx, r * 0.42, -r * 0.08, -r * 0.25, -r * 0.5, r * 0.3, -r * 0.04);
ctx.fill();
ctx.fillStyle = '#1d1408';
ell(ctx, r * 0.6, 0, 1.4, 1.4);
ctx.fill();
ctx.restore();
if (!chick) hpSliver(ctx, e);
}
/* ---------------- BOSS: T-Rex ---------------------------------------- */
function drawRex(ctx, b, t) {
var r = b.radius;
var col = b.hitFlash > 0 ? '#fff6ea' : b.color;
var dark = shade(col, -30);
var charging = b.charging > 0;
var jaw = charging ? 0.85 : 0.3 + 0.18 * Math.sin(t * 3.4);
var ph = t * (charging ? 9 : 5.5);
shadow(ctx, b.x, b.y, r * 1.45, r * 1.0);
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.face || 0);
taperTail(ctx, -r * 0.6, 0, r * 2.2, r * 0.55, Math.sin(t * 3.2 + (b.seed || 0)) * r * 0.3, dark);
// thick legs
ctx.fillStyle = dark;
ell(ctx, -r * 0.2 + Math.sin(ph) * r * 0.14, -r * 0.55, r * 0.34, r * 0.26, 0.2);
ctx.fill();
ell(ctx, -r * 0.2 - Math.sin(ph) * r * 0.14, r * 0.55, r * 0.34, r * 0.26, -0.2);
ctx.fill();
// body
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.05, r * 0.78);
ctx.fill();
ctx.fillStyle = 'rgba(255,230,190,0.14)';
ell(ctx, r * 0.1, 0, r * 0.72, r * 0.42);
ctx.fill();
// ridged spine scutes
ctx.fillStyle = shade(col, -45);
for (var i = 0; i < 6; i++) {
var sx = -r * 0.5 + i * r * 0.26;
tri(ctx, sx, -r * 0.72, sx + r * 0.09, -r * 0.95, sx + r * 0.18, -r * 0.72);
ctx.fill();
}
// tiny arms with claws
ctx.strokeStyle = dark;
ctx.lineWidth = r * 0.1;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(r * 0.62, -r * 0.34); ctx.lineTo(r * 0.95, -r * 0.48);
ctx.moveTo(r * 0.62, r * 0.34); ctx.lineTo(r * 0.95, r * 0.48);
ctx.stroke();
// massive skull with hinged jaw
ctx.fillStyle = col;
ell(ctx, r * 0.95, 0, r * 0.5, r * 0.42);
ctx.fill();
ctx.save();
ctx.translate(r * 1.1, 0);
// upper skull block
rr(ctx, -r * 0.18, -r * 0.4, r * 1.05, r * 0.4, r * 0.12);
ctx.fill();
// brow ridges
ctx.fillStyle = shade(col, -40);
ell(ctx, r * 0.28, -r * 0.34, r * 0.14, r * 0.07, -0.2);
ctx.fill();
ell(ctx, r * 0.28, r * 0.34, r * 0.14, r * 0.07, 0.2);
ctx.fill();
// lower jaw rotates open, red maw + teeth
ctx.save();
ctx.rotate(jaw);
ctx.fillStyle = dark;
rr(ctx, -r * 0.15, r * 0.02, r * 0.98, r * 0.3, r * 0.1);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#7e1d10';
tri(ctx, -r * 0.05, -r * 0.05, r * 0.8, -r * 0.02, r * 0.3, r * 0.3 * jaw + r * 0.05);
ctx.fill();
// teeth zig-zag
ctx.fillStyle = BONE;
for (var th = 0; th < 5; th++) {
var tx = th * r * 0.16;
tri(ctx, tx, -r * 0.02, tx + r * 0.05, r * 0.14 * (jaw + 0.4), tx + r * 0.1, -r * 0.02);
ctx.fill();
}
ctx.fillStyle = '#ffd24a';
ell(ctx, r * 0.3, -r * 0.26, 2.6, 2.6);
ctx.fill();
ctx.restore();
// charge dust
if (charging && Math.random() < 0.4) {
ctx.fillStyle = 'rgba(150,120,80,0.3)';
ell(ctx, -r, (Math.random() - 0.5) * r, r * 0.3, r * 0.18);
ctx.fill();
}
ctx.lineCap = 'butt';
ctx.restore();
}
/* ---------------- BOSS: Spinosaurus ---------------------------------- */
function drawSpino(ctx, b, t) {
var r = b.radius;
var col = b.hitFlash > 0 ? '#fff6ea' : b.color;
var dark = shade(col, -30);
shadow(ctx, b.x, b.y, r * 1.4, r * 0.9);
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.face || 0);
taperTail(ctx, -r * 0.6, 0, r * 2.3, r * 0.45, Math.sin(t * 3 + (b.seed || 0)) * r * 0.35, dark);
// dorsal SAIL — fan of spines along the back
ctx.fillStyle = dark;
for (var i = 0; i < 7; i++) {
var sx = -r * 0.55 + i * r * 0.24;
var hgt = r * (0.55 + Math.sin(i / 6 * Math.PI) * 0.5);
tri(ctx, sx, -r * 0.55, sx + r * 0.1, -r * 0.55 - hgt, sx + r * 0.2, -r * 0.55);
ctx.fill();
}
ctx.fillStyle = '#e04b3a';
for (var j = 1; j < 6; j += 2) {
var sx2 = -r * 0.55 + j * r * 0.24;
tri(ctx, sx2 + r * 0.03, -r * 0.55, sx2 + r * 0.1, -r * 0.55 - r * 0.28, sx2 + r * 0.17, -r * 0.55);
ctx.fill();
}
legsPair(ctx, -r * 0.15, t * 6 + (b.seed || 0), r * 0.9, r * 0.3, col, dark);
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.1, r * 0.55);
ctx.fill();
// crocodilian snout
ctx.fillStyle = col;
ell(ctx, r * 1.0, 0, r * 0.42, r * 0.32);
ctx.fill();
rr(ctx, r * 1.05, -r * 0.2, r * 0.95, r * 0.4, r * 0.16);
ctx.fill();
ctx.fillStyle = dark;
rr(ctx, r * 1.05, r * 0.04, r * 0.92, r * 0.2, r * 0.1);
ctx.fill();
ctx.strokeStyle = BONE;
ctx.lineWidth = 1.6;
ctx.beginPath();
for (var k = 0; k < 5; k++) {
var tx = r * 1.15 + k * r * 0.17;
ctx.moveTo(tx, r * 0.06);
ctx.lineTo(tx + r * 0.05, r * 0.16);
}
ctx.stroke();
ctx.fillStyle = '#ffd24a';
ell(ctx, r * 1.02, -r * 0.16, 2.4, 2.4);
ctx.fill();
ctx.restore();
}
/* ---------------- BOSS: Giganotosaurus (splitter) -------------------- */
function drawGiga(ctx, b, t) {
var r = b.radius;
var col = b.hitFlash > 0 ? '#fff6ea' : b.color;
var dark = shade(col, -32);
var jaw = 0.4 + 0.25 * Math.sin(t * 4);
shadow(ctx, b.x, b.y, r * 1.4, r * 0.95);
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.face || 0);
taperTail(ctx, -r * 0.6, 0, r * 2.0, r * 0.5, Math.sin(t * 3.6 + (b.seed || 0)) * r * 0.32, dark);
legsPair(ctx, -r * 0.15, t * 7 + (b.seed || 0), r * 0.95, r * 0.3, col, dark);
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.05, r * 0.7);
ctx.fill();
ctx.strokeStyle = 'rgba(120,20,10,0.55)';
ctx.lineWidth = r * 0.09;
for (var st = -1; st <= 1; st++) {
ctx.beginPath();
ctx.arc(st * r * 0.45, 0, r * 0.42, Math.PI * 0.7, Math.PI * 1.3);
ctx.stroke();
}
// longer three-fingered arms
ctx.strokeStyle = dark;
ctx.lineWidth = r * 0.11;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(r * 0.6, -r * 0.36); ctx.lineTo(r * 1.05, -r * 0.6);
ctx.moveTo(r * 0.6, r * 0.36); ctx.lineTo(r * 1.05, r * 0.6);
ctx.stroke();
// wide serrated skull
ctx.fillStyle = col;
ell(ctx, r * 0.95, 0, r * 0.5, r * 0.4);
ctx.fill();
ctx.save();
ctx.translate(r * 1.1, 0);
rr(ctx, -r * 0.15, -r * 0.36, r * 1.1, r * 0.36, r * 0.1);
ctx.fill();
ctx.save();
ctx.rotate(jaw);
ctx.fillStyle = dark;
rr(ctx, -r * 0.12, r * 0.02, r * 1.0, r * 0.28, r * 0.09);
ctx.fill();
ctx.restore();
ctx.fillStyle = BONE;
for (var th2 = 0; th2 < 6; th2++) {
var tx2 = th2 * r * 0.16;
tri(ctx, tx2, 0, tx2 + r * 0.04, r * 0.13 * (jaw + 0.3), tx2 + r * 0.08, 0);
ctx.fill();
}
ctx.fillStyle = '#ff5040';
ell(ctx, r * 0.32, -r * 0.24, 2.4, 2.4);
ctx.fill();
ctx.restore();
ctx.lineCap = 'butt';
ctx.restore();
}
/* ---------------- Stegosaurus (plates & thagomizer) ----------------- */
function drawStego(ctx, e, t) {
var r = e.radius;
var col = e.hitFlash > 0 ? '#fff6ea' : e.color;
var dark = shade(col, -28);
var ph = t * 6 + (e.seed || 0) * 7;
shadow(ctx, e.x, e.y, r * 1.3, r * 0.9);
ctx.save();
ctx.translate(e.x, e.y);
ctx.rotate(e.face || 0);
// thagomizer: four spikes fanning from the tail tip
var sway = Math.sin(t * 4 + (e.seed || 0)) * r * 0.25;
taperTail(ctx, -r * 0.75, 0, r * 1.5, r * 0.42, sway, dark);
ctx.strokeStyle = BONE;
ctx.lineWidth = 2.2;
ctx.lineCap = 'round';
for (var sp2 = -2; sp2 <= 2; sp2++) {
if (!sp2) continue;
var ta = Math.PI + sp2 * 0.38 + sway * 0.02;
ctx.beginPath();
ctx.moveTo(-r * 2.15, sway);
ctx.lineTo(-r * 2.15 + Math.cos(ta) * r * 0.55, sway + Math.sin(ta) * r * 0.55);
ctx.stroke();
}
ctx.lineCap = 'butt';
// legs
ctx.fillStyle = dark;
ell(ctx, -r * 0.35 + Math.sin(ph) * 3, -r * 0.58, r * 0.26, r * 0.2, 0.2);
ctx.fill();
ell(ctx, -r * 0.35 - Math.sin(ph) * 3, r * 0.58, r * 0.26, r * 0.2, -0.2);
ctx.fill();
// body
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.05, r * 0.8);
ctx.fill();
ctx.fillStyle = 'rgba(255,235,200,0.14)';
ell(ctx, -r * 0.05, 0, r * 0.7, r * 0.46);
ctx.fill();
// double row of back plates — kite shapes alternating sizes
for (var i = -3; i <= 3; i++) {
var px = i * r * 0.32;
var hgt = r * (0.5 + Math.sin((i + 3) / 6 * Math.PI) * 0.45);
ctx.fillStyle = i % 2 ? '#d98e2b' : '#c96f2a';
ctx.beginPath();
ctx.moveTo(px - r * 0.13, -r * 0.62);
ctx.quadraticCurveTo(px, -r * 0.62 - hgt * 1.25, px + r * 0.13, -r * 0.62);
ctx.closePath();
ctx.fill();
ctx.fillStyle = i % 2 ? '#b06f1e' : '#a85d20';
ctx.beginPath();
ctx.moveTo(px - r * 0.13, r * 0.62);
ctx.quadraticCurveTo(px, r * 0.62 + hgt * 1.25, px + r * 0.13, r * 0.62);
ctx.closePath();
ctx.fill();
}
// small head
ctx.fillStyle = col;
ell(ctx, r * 0.95, 0, r * 0.34, r * 0.26);
ctx.fill();
tri(ctx, r * 1.1, -r * 0.12, r * 1.5, 0, r * 1.1, r * 0.12);
ctx.fill();
ctx.fillStyle = '#1d1408';
ell(ctx, r * 0.98, -r * 0.12, 1.5, 1.5);
ctx.fill();
ctx.restore();
hpSliver(ctx, e);
}
/* ---------------- Pachycephalosaurus (head-butt charger) ------------- */
function drawPachy(ctx, e, t) {
var r = e.radius;
var col = e.hitFlash > 0 ? '#fff6ea' : e.color;
var dark = shade(col, -30);
var ph = t * 10 + (e.seed || 0) * 8;
// crouch low during the dash itself
var dashing = e.dashing > 0;
var windup = !dashing && e.dashCd !== undefined && e.dashCd < 0.45;
shadow(ctx, e.x, e.y, r * 1.2, r * 0.72);
ctx.save();
ctx.translate(e.x, e.y);
// dust when charging
if (dashing && Math.random() < 0.5) {
ctx.fillStyle = 'rgba(150,120,80,0.35)';
ell(ctx, (Math.random() - 0.5) * r, (Math.random() - 0.5) * r, r * 0.3, r * 0.16);
ctx.fill();
}
ctx.rotate(e.face || 0);
taperTail(ctx, -r * 0.65, 0, r * 1.3, r * 0.34, Math.sin(t * 8 + (e.seed || 0)) * r * 0.3, dark);
legsPair(ctx, -r * 0.15, dashing ? ph * 2 : ph, r, r * 0.28, col, dark);
ctx.fillStyle = col;
ell(ctx, 0, 0, r * 1.02, r * 0.56);
ctx.fill();
ctx.fillStyle = 'rgba(255,240,200,0.18)';
ell(ctx, r * 0.05, 0, r * 0.62, r * 0.26);
ctx.fill();
// stocky neck + the famous bone dome
ctx.fillStyle = col;
ell(ctx, r * 0.82, 0, r * 0.36, r * 0.32);
ctx.fill();
var domeC = windup ? '#ffe9bd' : BONE;
ctx.fillStyle = domeC;
ell(ctx, r * 1.08, 0, r * 0.4, r * 0.36);
ctx.fill();
ctx.strokeStyle = '#9c7444';
ctx.lineWidth = 1.6;
for (var k = -1; k <= 1; k++) {
ctx.beginPath();
ctx.arc(r * 1.08, 0, r * 0.26, k * 0.8 - 0.4, k * 0.8 + 0.4);
ctx.stroke();
}
// snout bumps & eye
ctx.fillStyle = dark;
ell(ctx, r * 1.38, 0, r * 0.14, r * 0.12);
ctx.fill();
ctx.fillStyle = windup || dashing ? '#ff5040' : '#1d1408';
ell(ctx, r * 0.92, -r * 0.16, 1.5, 1.5);
ctx.fill();
// warning "!" above while winding up
if (windup) {
ctx.rotate(-(e.face || 0));
ctx.fillStyle = '#ffb938';
ctx.font = 'bold ' + Math.round(r * 1.2) + 'px system-ui';
ctx.textAlign = 'center';
ctx.fillText('!', 0, -r * 1.7);
}
ctx.restore();
hpSliver(ctx, e);
}
/* ---------------- sky & drifting air -------------------------------- */
// big soft cloud shadows crawling across the ground
function drawCloudShadows(ctx, W, H, t) {
ctx.fillStyle = 'rgba(8,5,2,0.10)';
for (var i = 0; i < 4; i++) {
var cx = ((i * 467 + t * (11 + i * 4)) % (W + 520)) - 260;
var cy = (i * 231 + Math.sin(t * 0.05 + i) * 60) % (H + 300);
ell(ctx, cx, cy - 150, 190 + i * 34, 74 + i * 12, i * 0.6);
ctx.fill();
ell(ctx, cx + 120, cy - 110, 110 + i * 20, 48 + i * 9, i * 0.6);
ctx.fill();
}
}
// a vulture's shadow gliding across the arena now and then
function drawVultureShadow(ctx, W, H, t) {
var T = 26;
var p = (t % T) / T;
if (p > 0.32) return; // visible only ~8s per cycle
var q = p / 0.32;
var vx = -140 + q * (W + 280);
var vy = H * 0.3 + Math.sin(q * Math.PI) * H * 0.25;
var sc = 1.6 + q * 0.8;
ctx.fillStyle = 'rgba(8,5,2,0.16)';
ell(ctx, vx, vy, 30 * sc, 10 * sc, 0.06);
ctx.fill();
// wing beats: shadow width pulses
ctx.fillStyle = 'rgba(8,5,2,0.13)';
ell(ctx, vx - 14 * sc, vy - 3, 14 * sc, 5 * sc, -0.25);
ctx.fill();
ell(ctx, vx + 14 * sc, vy - 3, 14 * sc, 5 * sc, 0.25);
ctx.fill();
}
// floating ash / pollen motes catching the light
function drawAshMotes(ctx, W, H, t) {
for (var i = 0; i < 26; i++) {
var sd = i * 173.3;
var mx = (sd * 7.13 + t * (8 + (i % 5) * 4)) % (W + 40) - 20;
var my = (sd * 3.77 + Math.sin(t * 0.7 + i) * 24 + i * 31) % (H + 40) - 20;
var tw = 0.5 + 0.5 * Math.sin(t * 2.4 + i * 1.9);
ctx.globalAlpha = 0.10 + tw * 0.16;
ctx.fillStyle = i % 3 === 0 ? '#ffb96a' : '#d8ccb0';
ell(ctx, mx, my, 1.6 + (i % 3) * 0.5, 1.6 + (i % 3) * 0.5, 0);
ctx.fill();
}
ctx.globalAlpha = 1;
}
/* ---------------- tropical rain -------------------------------------- */
// Stateless streak field + ground splashes; intensity 0..1.
function drawRain(ctx, W, H, t, intensity) {
var n = Math.floor(72 * (intensity || 1));
ctx.strokeStyle = 'rgba(178,198,235,0.34)';
ctx.lineWidth = 1.4;
ctx.beginPath();
for (var i = 0; i < n; i++) {
var sd = i * 97.31;
var x = ((sd * 13.7 + t * (360 + (i % 40) * 9)) % (W + 180)) - 90;
var y = ((sd * 29.17 + t * 760) % (H + 90)) - 45;
var len = 15 + (i % 12);
ctx.moveTo(x, y);
ctx.lineTo(x - len * 0.28, y + len);
}
ctx.stroke();
// splashes near the ground
ctx.fillStyle = 'rgba(190,210,240,0.16)';
for (var sp4 = 0; sp4 < 16; sp4++) {
var sd2 = sp4 * 61.7;
var sx = ((sd2 * 17.3 + t * 300) % (W + 60)) - 30;
var sy = H - 26 - ((sd2 * 7.9) % 40);
var pr3 = 1.5 + ((t * 6 + sp4) % 1) * 2.5;
ell(ctx, sx, sy, pr3 * 1.7, pr3 * 0.7, 0);
ctx.fill();
}
// wet air tint
ctx.fillStyle = 'rgba(140,168,215,0.05)';
ctx.fillRect(0, 0, W, H);
}
/* ---------------- nightfall ----------------------------------------- */
// Darkness floods the valley on a 60s cycle; warm holes are punched out
// around every fire source so the tribe's flames truly matter at night.
function drawNight(ctx, W, H, g) {
var dk = g.darkness || 0;
if (dk <= 0.02) return;
// One even veil over the whole valley. No punched holes, no light
// pools — those always read as big circles on open ground.
ctx.fillStyle = 'rgba(9,11,26,' + (dk * 0.85).toFixed(3) + ')';
ctx.fillRect(0, 0, W, H);
// Only TIGHT halos hugging actual flames — never floor-wide discs.
function flameGlow(x, y, r, flickerT, seed) {
var rr = r + Math.sin(flickerT * 9 + seed) * 6;
var gr = ctx.createRadialGradient(x, y, 0, x, y, rr);
gr.addColorStop(0, 'rgba(255,170,80,' + (0.34 * dk / 0.74).toFixed(3) + ')');
gr.addColorStop(0.45, 'rgba(255,140,60,' + (0.16 * dk / 0.74).toFixed(3) + ')');
gr.addColorStop(1, 'rgba(255,120,50,0)');
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = gr;
ctx.fillRect(x - rr, y - rr, rr * 2, rr * 2);
ctx.globalCompositeOperation = 'source-over';
}
// campfire crown
flameGlow(W / 2, H / 2 - 12, 95, g.time, 0);
// totem flames
if (g.decor && g.decor.totems) {
for (var ti = 0; ti < g.decor.totems.length; ti++) {
var tt = g.decor.totems[ti];
flameGlow(tt.x, tt.y - 40, 42, g.time, tt.x);
}
}
// the hero's torch head
if (g.player && g.player.alive && g.torchGlow !== false) {
var tx2 = g.player.x + Math.cos((g.player.aim || 0) - 0.5) * 16;
var ty2 = g.player.y + Math.sin((g.player.aim || 0) - 0.5) * 16 - 14;
flameGlow(tx2, ty2, 52, g.time, 7.7);
}
}
/* ---------------- attract-mode dusk scene (menu backdrop) ----------- */
function drawMenuScene(ctx, w, h, t) {
// vivid dusk sky
var sky = ctx.createLinearGradient(0, 0, 0, h);
sky.addColorStop(0, '#221033');
sky.addColorStop(0.38, '#542410');
sky.addColorStop(0.66, '#b4581c');
sky.addColorStop(0.82, '#d97a26');
sky.addColorStop(1, '#241206');
ctx.fillStyle = sky;
ctx.fillRect(0, 0, w, h);
// first stars twinkling in the purple band
for (var st4 = 0; st4 < 46; st4++) {
var sx4 = ((st4 * 211.7) % w + w) % w;
var sy4 = ((st4 * 89.3) % (h * 0.34));
var tw4 = 0.35 + 0.65 * Math.abs(Math.sin(t * 2.6 + st4 * 1.71));
ctx.globalAlpha = tw4;
ctx.fillStyle = '#ffe9c9';
ctx.fillRect(sx4, sy4, 1.8, 1.8);
}
ctx.globalAlpha = 1;
// low blood sun with wide halo
var sunX = w * 0.72, sunY = h * 0.60;
var halo = ctx.createRadialGradient(sunX, sunY, h * 0.02, sunX, sunY, h * 0.30);
halo.addColorStop(0, 'rgba(255,200,120,0.55)');
halo.addColorStop(0.5, 'rgba(255,150,70,0.22)');
halo.addColorStop(1, 'rgba(255,120,50,0)');
ctx.fillStyle = halo;
ctx.fillRect(sunX - h * 0.32, sunY - h * 0.32, h * 0.64, h * 0.64);
ctx.fillStyle = '#ffd98f';
ell(ctx, sunX, sunY, h * 0.085, h * 0.085, 0);
ctx.fill();
ctx.fillStyle = 'rgba(255,240,210,0.85)';
ell(ctx, sunX - h * 0.02, sunY - h * 0.02, h * 0.05, h * 0.05, 0);
ctx.fill();
// far volcano, smoking
ctx.fillStyle = '#160c05';
tri(ctx, w * 0.80, h * 0.66, w * 0.97, h * 0.66, w * 0.885, h * 0.30);
ctx.fill();
ctx.fillStyle = 'rgba(255,110,50,' + (0.75 + 0.25 * Math.sin(t * 2.2)).toFixed(2) + ')';
tri(ctx, w * 0.862, h * 0.350, w * 0.908, h * 0.350, w * 0.885, h * 0.308);
ctx.fill();
for (var em = 0; em < 5; em++) {
var cyc = (t * 0.5 + em / 5) % 1;
ctx.globalAlpha = (1 - cyc) * 0.7;
ctx.fillStyle = '#ff8a3d';
ell(ctx,
w * 0.885 + Math.sin(t * 2 + em * 2.1) * 14 * cyc,
h * 0.315 - cyc * h * 0.22, 2.2, 2.2, 0);
ctx.fill();
}
ctx.globalAlpha = 1;
// mid-ground plain band with a warm horizon rim so silhouettes pop
ctx.fillStyle = '#2a1a0e';
ctx.fillRect(0, h * 0.64, w, h);
ctx.fillStyle = 'rgba(255,150,70,0.30)';
ctx.fillRect(0, h * 0.64, w, 5);
/* every creature is a dark shape RIM-LIT by the sunset — visible! */
function rimPath(fillCol, strokeCol, lw) {
ctx.fillStyle = fillCol;
ctx.fill();
if (strokeCol) {
ctx.strokeStyle = strokeCol;
ctx.lineWidth = lw || 2;
ctx.stroke();
}
}
// ---- distant brachiosaurus against the bright sky, swaying slowly ----
var bx = w * 0.15 + Math.sin(t * 0.11) * 12;
ctx.save();
ctx.translate(bx, h * 0.665);
ctx.beginPath();
ctx.moveTo(-64, 0); // tail tip on the ground
ctx.quadraticCurveTo(-38, -10, -22, -42); // up the back
ctx.quadraticCurveTo(-14, -66, 5, -74); // neck rise
ctx.quadraticCurveTo(15, -79, 19, -98 + Math.sin(t * 0.5) * 4); // small head high
ctx.lineTo(26, -95);
ctx.quadraticCurveTo(21, -70, 28, -55); // neck front
ctx.quadraticCurveTo(43, -24, 50, 0); // chest to ground
ctx.closePath();
rimPath('#1c1108', 'rgba(255,150,70,0.85)', 2.4);
// legs (rim-lit too)
ctx.fillStyle = '#1c1108';
ctx.strokeStyle = 'rgba(255,150,70,0.55)';
ctx.lineWidth = 1.6;
ctx.fillRect(-33, -18, 9, 18); ctx.strokeRect(-33, -18, 9, 18);
ctx.fillRect(21, -18, 9, 18); ctx.strokeRect(21, -18, 9, 18);
ctx.restore();
// ---- T-REX stalking toward the sun — the star of the menu ----
var trx = w * 0.47 + Math.sin(t * 0.23) * 16;
ctx.save();
ctx.translate(trx, h * 0.685);
ctx.beginPath();
ctx.moveTo(-78, 0); // heavy tail on the ground
ctx.quadraticCurveTo(-46, -16, -26, -38); // tail up to hips
ctx.quadraticCurveTo(-16, -52, -2, -56); // back
ctx.quadraticCurveTo(10, -60, 18, -72); // neck up
ctx.quadraticCurveTo(24, -84, 40, -88); // big square head
ctx.lineTo(44, -80); // brow dip
ctx.lineTo(38, -76);
ctx.lineTo(46, -68); // open upper jaw
ctx.quadraticCurveTo(30, -62, 22, -60); // jaw hinge
ctx.quadraticCurveTo(20, -48, 14, -34); // chest down
ctx.quadraticCurveTo(10, -26, 12, -16); // thigh mass
ctx.lineTo(20, 0); // standing leg
ctx.lineTo(6, 0);
ctx.lineTo(2, -14);
ctx.quadraticCurveTo(-8, -20, -14, -12); // belly
ctx.lineTo(-10, 0); // far leg
ctx.lineTo(-22, 0);
ctx.quadraticCurveTo(-30, -18, -40, -14); // rear
ctx.closePath();
rimPath('#160c06', 'rgba(255,140,60,0.9)', 2.6);
// tiny fierce arm
ctx.strokeStyle = 'rgba(255,150,70,0.8)';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(14, -40); ctx.lineTo(24, -36); ctx.lineTo(22, -30); ctx.stroke();
ctx.lineCap = 'butt';
// eye glint
ctx.fillStyle = '#ffd98f';
ctx.fillRect(32, -82, 3, 3);
ctx.restore();
// ---- raptor pack: bigger, warmer, clearly readable ----
for (var rp = 0; rp < 3; rp++) {
var rx = ((t * (52 + rp * 11)) % (w + 320)) - 160;
var ry = h * (0.875 + rp * 0.035) + Math.abs(Math.sin(t * 9 + rp * 1.4)) * -5;
var rs = (1.35 + rp * 0.22);
ctx.save();
ctx.translate(rx, ry);
ctx.scale(rs, rs);
ctx.beginPath();
ctx.moveTo(-30, 0);
ctx.quadraticCurveTo(-16, -8, -7, -15);
ctx.quadraticCurveTo(5, -22, 12, -20); // body
ctx.quadraticCurveTo(19, -26, 24, -32); // neck
ctx.quadraticCurveTo(31, -35, 33, -30); // head
ctx.lineTo(26, -25);
ctx.quadraticCurveTo(17, -14, 15, 0);
ctx.closePath();
rimPath(rp === 1 ? '#2a1509' : '#201006', 'rgba(255,130,45,' + (0.75 + rp * 0.08).toFixed(2) + ')', 1.8);
ctx.restore();
}
// foreground grass blades, swaying
ctx.strokeStyle = '#0a0603';
ctx.lineWidth = 3.4;
ctx.lineCap = 'round';
for (var gb = 0; gb < Math.floor(w / 26); gb++) {
var gx = gb * 26 + (gb % 3) * 7;
var gh = 22 + (gb % 4) * 9;
var sw2 = Math.sin(t * 1.6 + gb) * 5;
ctx.beginPath();
ctx.moveTo(gx, h);
ctx.quadraticCurveTo(gx + sw2 * 0.4, h - gh * 0.6, gx + sw2, h - gh);
ctx.stroke();
}
ctx.lineCap = 'butt';
// vignette so UI text stays readable
var vg2 = ctx.createRadialGradient(w / 2, h / 2, h * 0.30, w / 2, h / 2, h * 0.95);
vg2.addColorStop(0, 'rgba(0,0,0,0)');
vg2.addColorStop(1, 'rgba(5,3,1,0.38)');
ctx.fillStyle = vg2;
ctx.fillRect(0, 0, w, h);
}
/* ================================================================
Color helper
================================================================ */
function shade(hex, amt) {
var n = parseInt(hex.slice(1), 16);
var r = Math.max(0, Math.min(255, (n >> 16) + amt));
var g2 = Math.max(0, Math.min(255, ((n >> 8) & 255) + amt));
var b2 = Math.max(0, Math.min(255, (n & 255) + amt));
return '#' + ((1 << 24) + (r << 16) + (g2 << 8) + b2).toString(16).slice(1);
}
/* ================================================================
Pickups & projectiles (thematic)
================================================================ */
function drawPickup(ctx, pk, blinkOn) {
if (!blinkOn) return;
ctx.save();
ctx.translate(pk.x, pk.y + Math.sin(pk.t) * 2);
if (pk.kind === 'coin') {
// amber nugget
shadow(ctx, 0, 3, 8, 4);
ctx.fillStyle = '#ffb938';
ctx.strokeStyle = '#a86a12';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-7, -2); ctx.lineTo(-2, -7); ctx.lineTo(6, -5);
ctx.lineTo(7, 3); ctx.lineTo(1, 7); ctx.lineTo(-6, 4);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,230,0.85)';
ell(ctx, -2, -3, 2, 1.4, -0.4);
ctx.fill();
} else if (pk.kind === 'heal') {
// meat drumstick
shadow(ctx, 0, 3, 9, 4);
ctx.strokeStyle = BONE;
ctx.lineWidth = 4;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(2, 2); ctx.lineTo(9, 9);
ctx.stroke();
ctx.fillStyle = BONE;
ell(ctx, 10, 10, 2.6, 2.6);
ctx.fill();
ell(ctx, 12, 7, 2.2, 2.2);
ctx.fill();
ctx.fillStyle = '#b3502e';
ell(ctx, -1, -1, 7, 5.6, -0.7);
ctx.fill();
ctx.fillStyle = 'rgba(255,220,190,0.35)';
ell(ctx, -3, -3, 2.6, 1.6, -0.7);
ctx.fill();
ctx.lineCap = 'butt';
} else {
// fire bomb: round stone with burning fuse
shadow(ctx, 0, 3, 9, 4);
ctx.fillStyle = '#3c332a';
ell(ctx, 0, 1, 8, 7);
ctx.fill();
ctx.strokeStyle = '#211b14';
ctx.lineWidth = 2;
ctx.stroke();
ctx.strokeStyle = '#7a5230';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(2, -6);
ctx.quadraticCurveTo(6, -11, 4, -14);
ctx.stroke();
var sp = 1 + Math.abs(Math.sin(pk.t * 6)) * 2;
ctx.fillStyle = '#ffb938';
ell(ctx, 4, -15, sp, sp);
ctx.fill();
ctx.fillStyle = '#e04b3a';
ell(ctx, 4, -15, sp * 0.5, sp * 0.5);
ctx.fill();
}
ctx.restore();
}
function drawBullet(ctx, b) {
var kind = b.kind || 'stone';
var dx = b.vx, dy = b.vy;
var L = Math.hypot(dx, dy) || 1;
dx /= L; dy /= L;
// motion streak
ctx.strokeStyle = b.crit ? 'rgba(255,185,56,0.55)' : 'rgba(255,240,215,0.28)';
ctx.lineWidth = b.size;
ctx.beginPath();
ctx.moveTo(b.x - dx * b.size * 2.6, b.y - dy * b.size * 2.6);
ctx.lineTo(b.x, b.y);
ctx.stroke();
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(Math.atan2(dy, dx));
switch (kind) {
case 'blowpipe': // dart
ctx.fillStyle = '#caa96a';
ctx.fillRect(-7, -1.1, 12, 2.2);
ctx.fillStyle = BONE;
tri(ctx, 5, -2.4, 10, 0, 5, 2.4);
ctx.fill();
break;
case 'spears': // mini spear
ctx.fillStyle = '#8a6234';
ctx.fillRect(-9, -1.6, 15, 3.2);
ctx.fillStyle = stoneCol;
tri(ctx, 6, -3, 13, 0, 6, 3);
ctx.fill();
break;
case 'obelisk': // obsidian shard
ctx.fillStyle = '#3f3850';
ctx.beginPath();
ctx.moveTo(-8, -3); ctx.lineTo(9, 0); ctx.lineTo(-8, 3);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(199,146,255,0.5)';
ctx.fillRect(-6, -0.8, 12, 1.6);
break;
case 'firebomb': // flame ball
ctx.fillStyle = '#ffb938';
ell(ctx, 0, 0, b.size * 1.3, b.size);
ctx.fill();
ctx.fillStyle = '#e04b3a';
ell(ctx, -b.size * 0.4, 0, b.size * 0.7, b.size * 0.6);
ctx.fill();
break;
case 'venom': // enemy spit
ctx.fillStyle = b.color || '#8ac74a';
ell(ctx, 0, 0, b.size * 1.15, b.size * 0.85);
ctx.fill();
ctx.fillStyle = 'rgba(255,255,220,0.4)';
ell(ctx, -b.size * 0.5, -b.size * 0.3, b.size * 0.35, b.size * 0.25);
ctx.fill();
break;
default: // sling stone
ctx.fillStyle = '#8f8578';
ell(ctx, 0, 0, b.size * 1.05, b.size * 0.9);
ctx.fill();
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ell(ctx, b.size * 0.3, b.size * 0.25, b.size * 0.35, b.size * 0.25);
ctx.fill();
}
ctx.restore();
}
US.art = {
makeDecor: makeDecor,
drawGround: drawGround,
drawHuman: drawHuman,
drawRaptor: drawRaptor,
drawTriceratops: drawTriceratops,
drawStego: drawStego,
drawPachy: drawPachy,
drawDilo: drawDilo,
drawPtero: drawPtero,
drawRex: drawRex,
drawSpino: drawSpino,
drawGiga: drawGiga,
drawPickup: drawPickup,
drawBullet: drawBullet,
shade: shade,
drawCloudShadows: drawCloudShadows,
drawVultureShadow: drawVultureShadow,
drawAshMotes: drawAshMotes,
drawCampfire: drawCampfire,
drawNight: drawNight,
drawHeldWeapon: drawHeldWeapon,
drawRain: drawRain,
drawBiomeFx: drawBiomeFx,
BIOMES: BIOMES,
drawMenuScene: drawMenuScene
};
})();