/* 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 }; })();