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.
1930 lines
64 KiB
JavaScript
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
|
|
};
|
|
})();
|