- Isometric canvas RTS vs dinosaur waves (fan demake of Repterra) - Economy: houses/taxes, farms, foresters, quarries; colonist staffing - Power grid: generators extend build range; brownout + recovery - Defense: walls/gates, watchtowers (AA), cannon towers (ground-only) - Taming: Primal Pen + Tamers collar weakened dinos; pets obey commands - Breeding: tamed pairs incubate eggs at the pen; hatchlings grow up - 7 dino species incl. flying Pteranodons and lake-raiding Suchomimus - Telegraphed waves with direction arrows; day-15 final horde; 3 difficulties - Day/night cycle, fog of war, minimap, synth audio, 1x-3x speeds - Save/Load/Continue + dawn autosave (full JSON state snapshots) - Tests: 80-assertion headless suite, browser boot + E2E, balance harness
1241 lines
48 KiB
JavaScript
1241 lines
48 KiB
JavaScript
/* =========================================================
|
|
* REPRTERRA WEB — render.js
|
|
* Isometric canvas renderer. Everything hand-drawn, no assets.
|
|
* ========================================================= */
|
|
'use strict';
|
|
window.RTS = window.RTS || {};
|
|
|
|
RTS.render = (function () {
|
|
const U = RTS.util;
|
|
const C = RTS.CONFIG;
|
|
const W = C.WORLD.W, H = C.WORLD.H;
|
|
const TW = C.WORLD.TILE_W, TH = C.WORLD.TILE_H;
|
|
const TW2 = TW / 2, TH2 = TH / 2;
|
|
|
|
let cv = null, ctx = null;
|
|
let vw = 0, vh = 0, dpr = 1;
|
|
|
|
const S = {};
|
|
S.cam = { x: W / 2, y: H / 2, zoom: 1 };
|
|
|
|
S.init = function (canvas) {
|
|
cv = canvas;
|
|
ctx = cv.getContext('2d');
|
|
resize();
|
|
};
|
|
|
|
S.resize = resize;
|
|
function resize() {
|
|
if (!cv) return;
|
|
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
vw = cv.clientWidth; vh = cv.clientHeight;
|
|
cv.width = Math.floor(vw * dpr);
|
|
cv.height = Math.floor(vh * dpr);
|
|
}
|
|
|
|
function projX(wx, wy) { return (wx - wy) * TW2 * S.cam.zoom; }
|
|
function projY(wx, wy) { return (wx + wy) * TH2 * S.cam.zoom; }
|
|
|
|
S.worldToScreen = function (wx, wy) {
|
|
return {
|
|
x: projX(wx, wy) - projX(S.cam.x, S.cam.y) + vw / 2,
|
|
y: projY(wx, wy) - projY(S.cam.x, S.cam.y) + vh / 2,
|
|
};
|
|
};
|
|
S.screenToWorld = function (sx, sy) {
|
|
const z = S.cam.zoom;
|
|
const a = (sx - vw / 2) / (TW2 * z) + (S.cam.x - S.cam.y);
|
|
const b = (sy - vh / 2) / (TH2 * z) + (S.cam.x + S.cam.y);
|
|
return { x: (a + b) / 2, y: (b - a) / 2 };
|
|
};
|
|
|
|
S.clampCam = function () {
|
|
S.cam.x = U.clamp(S.cam.x, 2, W - 2);
|
|
S.cam.y = U.clamp(S.cam.y, 2, H - 2);
|
|
S.cam.zoom = U.clamp(S.cam.zoom, 0.45, 2.4);
|
|
};
|
|
|
|
// =========================================================
|
|
function diamond(sx, sy, w, h) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(sx, sy);
|
|
ctx.lineTo(sx + w, sy + h);
|
|
ctx.lineTo(sx, sy + 2 * h);
|
|
ctx.lineTo(sx - w, sy + h);
|
|
ctx.closePath();
|
|
}
|
|
|
|
// iso box: center world (cx,cy), half-size hf (tiles), height px
|
|
function isoBox(cx, cy, hf, hpx, cTop, cL, cR, lift) {
|
|
const z = S.cam.zoom;
|
|
const p = (dx, dy) => S.worldToScreen(cx + dx, cy + dy);
|
|
const A = p(-hf, -hf), B = p(hf, -hf), Cp = p(hf, hf), D = p(-hf, hf);
|
|
const L = (lift || 0) * S.cam.zoom;
|
|
// left wall (D->C)
|
|
ctx.fillStyle = cL;
|
|
ctx.beginPath();
|
|
ctx.moveTo(D.x, D.y - L); ctx.lineTo(Cp.x, Cp.y - L);
|
|
ctx.lineTo(Cp.x, Cp.y - L - hpx * z); ctx.lineTo(D.x, D.y - L - hpx * z);
|
|
ctx.closePath(); ctx.fill();
|
|
// right wall (C->B)
|
|
ctx.fillStyle = cR;
|
|
ctx.beginPath();
|
|
ctx.moveTo(Cp.x, Cp.y - L); ctx.lineTo(B.x, B.y - L);
|
|
ctx.lineTo(B.x, B.y - L - hpx * z); ctx.lineTo(Cp.x, Cp.y - L - hpx * z);
|
|
ctx.closePath(); ctx.fill();
|
|
// top
|
|
ctx.fillStyle = cTop;
|
|
ctx.beginPath();
|
|
ctx.moveTo(A.x, A.y - L - hpx * z); ctx.lineTo(B.x, B.y - L - hpx * z);
|
|
ctx.lineTo(Cp.x, Cp.y - L - hpx * z); ctx.lineTo(D.x, D.y - L - hpx * z);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
return { A, B, C: Cp, D, topY: A.y - L - hpx * z };
|
|
}
|
|
|
|
// =========================================================
|
|
// MAIN DRAW
|
|
// =========================================================
|
|
S.draw = function (st, ui, timeSec) {
|
|
if (!ctx) return;
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
ctx.clearRect(0, 0, vw, vh);
|
|
|
|
// screen shake
|
|
let shx = 0, shy = 0;
|
|
if (st.shakeT > 0) {
|
|
shx = (Math.random() - 0.5) * st.shakeMag * st.shakeT * 2;
|
|
shy = (Math.random() - 0.5) * st.shakeMag * st.shakeT * 2;
|
|
ctx.translate(shx, shy);
|
|
}
|
|
|
|
drawTerrain(st, timeSec);
|
|
drawDecals(st);
|
|
if (ui.placing) drawPlacementGuides(st, ui);
|
|
|
|
// ---- gather depth-sorted drawables ----
|
|
const items = [];
|
|
const T = st.world.tiles;
|
|
for (let i = 0; i < W * H; i++) {
|
|
if (T.tree[i] > 0 || T.rock[i] > 0) {
|
|
const x = i % W, y = (i / W) | 0;
|
|
if (st.vis[i] === 0) continue;
|
|
items.push({ d: 0, x: x + 0.5, y: y + 0.5, t: T.tree[i] > 0 ? 'tree' : 'rock', v: T.tree[i] > 0 ? T.tree[i] : T.rock[i], vx: x, vy: y });
|
|
}
|
|
}
|
|
for (const b of st.buildings) {
|
|
if (b.dead) continue;
|
|
items.push({ d: 1, x: b.x, y: b.y + b.size * 0.5, t: 'bld', b });
|
|
}
|
|
for (const u of st.units) {
|
|
if (u.dead) continue;
|
|
const ti = (u.y | 0) * W + (u.x | 0);
|
|
if (U.inBounds(u.x | 0, u.y | 0, W, H) && st.vis[ti] === 0) continue;
|
|
items.push({ d: 2, x: u.x, y: u.y, t: 'unit', u });
|
|
}
|
|
for (const d of st.dinos) {
|
|
if (d.dead) continue;
|
|
const ti = (d.y | 0) * W + (d.x | 0);
|
|
if (U.inBounds(d.x | 0, d.y | 0, W, H) && st.vis[ti] === 0) continue;
|
|
items.push({ d: 3, x: d.x, y: d.y + (d.flying ? 0.01 : 0), t: 'dino', d });
|
|
}
|
|
for (const e of st.eggs || []) {
|
|
const ti = (e.y | 0) * W + (e.x | 0);
|
|
if (U.inBounds(e.x | 0, e.y | 0, W, H) && st.vis[ti] === 0) continue;
|
|
items.push({ d: 2, x: e.x, y: e.y + 0.02, t: 'egg', e });
|
|
}
|
|
items.sort((a, b) => (a.y - b.y) || (a.x - b.x) || (a.d - b.d));
|
|
for (const it of items) {
|
|
if (it.t === 'tree') drawTree(it.x, it.y, it.v, timeSec, it.vx, it.vy);
|
|
else if (it.t === 'rock') drawRock(it.x, it.y, it.v);
|
|
else if (it.t === 'bld') drawBuilding(st, it.b, ui, timeSec);
|
|
else if (it.t === 'unit') drawUnit(it.u, timeSec);
|
|
else if (it.t === 'egg') drawEgg(it.e, timeSec);
|
|
else drawDino(it.d, timeSec);
|
|
}
|
|
|
|
drawProjectiles(st, timeSec);
|
|
drawParticles(st);
|
|
drawSelectionOverlays(st, ui);
|
|
if (ui.placing) drawGhost(st, ui);
|
|
drawFog(st);
|
|
drawNightTint(st);
|
|
drawTexts(st);
|
|
|
|
// wave warning arrows
|
|
if (st.warnT > 0 && !st.over) drawWarningArrows(st, timeSec);
|
|
};
|
|
|
|
// ---------------------------------------------------------
|
|
function drawTerrain(st, timeSec) {
|
|
const T = st.world.tiles;
|
|
const z = S.cam.zoom;
|
|
// visible world bounds (4 screen corners -> world)
|
|
const c0 = S.screenToWorld(0, 0), c1 = S.screenToWorld(vw, 0);
|
|
const c2 = S.screenToWorld(0, vh), c3 = S.screenToWorld(vw, vh);
|
|
const minX = Math.max(0, Math.floor(Math.min(c0.x, c1.x, c2.x, c3.x)) - 1);
|
|
const maxX = Math.min(W - 1, Math.ceil(Math.max(c0.x, c1.x, c2.x, c3.x)) + 1);
|
|
const minY = Math.max(0, Math.floor(Math.min(c0.y, c1.y, c2.y, c3.y)) - 1);
|
|
const maxY = Math.min(H - 1, Math.ceil(Math.max(c0.y, c1.y, c2.y, c3.y)) + 2);
|
|
|
|
for (let y = minY; y <= maxY; y++) {
|
|
for (let x = minX; x <= maxX; x++) {
|
|
const i = y * W + x;
|
|
const p = S.worldToScreen(x + 0.5, y + 0.5);
|
|
if (p.x < -TW || p.x > vw + TW || p.y < -TH * 2 || p.y > vh + TH * 2) continue;
|
|
const t = T.terrain[i];
|
|
const v = T.variant[i];
|
|
let col;
|
|
if (t === 3) {
|
|
const wob = Math.sin(timeSec * 1.5 + v * 20 + x * 0.7 + y * 0.9) * 6;
|
|
col = 'hsl(205,45%,' + (26 + wob) + '%)';
|
|
} else if (t === 2) col = shade('#8a795a', v * 8 - 4);
|
|
else if (t === 1) col = shade('#55743a', v * 10 - 5);
|
|
else col = shade('#5f8142', v * 10 - 5);
|
|
diamond(p.x, p.y, TW2 * z, TH2 * z);
|
|
ctx.fillStyle = col;
|
|
ctx.fill();
|
|
if (v > 0.62 && t !== 3) { // speckle detail
|
|
ctx.fillStyle = 'rgba(255,255,255,0.04)';
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function shade(hex, amt) {
|
|
const n = parseInt(hex.slice(1), 16);
|
|
let r = (n >> 16) + amt, g = ((n >> 8) & 255) + amt, b = (n & 255) + amt;
|
|
r = U.clamp(r, 0, 255); g = U.clamp(g, 0, 255); b = U.clamp(b, 0, 255);
|
|
return 'rgb(' + (r | 0) + ',' + (g | 0) + ',' + (b | 0) + ')';
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawDecals(st) {
|
|
for (const d of st.decals) {
|
|
const p = S.worldToScreen(d.x, d.y);
|
|
ctx.globalAlpha = d.alpha;
|
|
ctx.fillStyle = d.color;
|
|
if (d.rubble) {
|
|
const z = S.cam.zoom * (d.size || 1);
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y, 18 * z, 9 * z, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#3a352c';
|
|
for (let k = 0; k < 3; k++) {
|
|
ctx.fillRect(p.x - 10 * z + k * 9 * z, p.y - 4 * z + (k % 2) * 4 * z, 6 * z, 3 * z);
|
|
}
|
|
} else {
|
|
const z = S.cam.zoom;
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y, d.r * TW2 * z, d.r * TH2 * z, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawTree(x, y, v, timeSec, tx, ty) {
|
|
const p = S.worldToScreen(x, y);
|
|
const z = S.cam.zoom;
|
|
const s = (v / 8);
|
|
const sway = Math.sin(timeSec * 1.2 + x * 3 + y * 2) * 1.5;
|
|
// shadow
|
|
ctx.fillStyle = 'rgba(0,0,0,0.22)';
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y + 2 * z, 13 * s * z, 6 * s * z, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// trunk
|
|
ctx.fillStyle = '#5d452c';
|
|
ctx.fillRect(p.x - 2 * z, p.y - 14 * s * z, 4 * z, 14 * s * z);
|
|
// canopy blobs
|
|
const cy = p.y - 16 * s * z;
|
|
ctx.fillStyle = '#39602f';
|
|
ctx.beginPath(); ctx.ellipse(p.x + sway * z, cy - 6 * s * z, 13 * s * z, 11 * s * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#4a7839';
|
|
ctx.beginPath(); ctx.ellipse(p.x - 4 * z + sway * z, cy - 10 * s * z, 8 * s * z, 7 * s * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#588c44';
|
|
ctx.beginPath(); ctx.ellipse(p.x + 5 * z + sway * z, cy - 4 * s * z, 6.5 * s * z, 5.5 * s * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
|
|
function drawRock(x, y, v) {
|
|
const p = S.worldToScreen(x, y);
|
|
const z = S.cam.zoom;
|
|
const s = (v / 10) * 0.8 + 0.35;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.22)';
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y + 1 * z, 15 * s * z, 7 * s * z, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#7d8288';
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x - 13 * s * z, p.y);
|
|
ctx.lineTo(p.x - 7 * s * z, p.y - 12 * s * z);
|
|
ctx.lineTo(p.x + 2 * s * z, p.y - 14 * s * z);
|
|
ctx.lineTo(p.x + 11 * s * z, p.y - 6 * s * z);
|
|
ctx.lineTo(p.x + 12 * s * z, p.y);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = '#98a0a8';
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x - 7 * s * z, p.y - 12 * s * z);
|
|
ctx.lineTo(p.x + 2 * s * z, p.y - 14 * s * z);
|
|
ctx.lineTo(p.x + 5 * s * z, p.y - 8 * s * z);
|
|
ctx.lineTo(p.x - 3 * s * z, p.y - 7 * s * z);
|
|
ctx.closePath(); ctx.fill();
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawBuilding(st, b, ui, timeSec) {
|
|
const def = C.BUILDINGS[b.defId];
|
|
const hf = b.size / 2 - 0.08;
|
|
const sel = ui.sel && ui.sel.kind === 'building' && ui.sel.id === b.id;
|
|
const off = !b.powered && b.done;
|
|
|
|
if (sel) {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
diamond(p.x, p.y, (b.size / 2) * TW2 * S.cam.zoom * 1.06, (b.size / 2) * TH2 * S.cam.zoom * 1.06);
|
|
ctx.strokeStyle = '#ffe08a'; ctx.lineWidth = 2; ctx.stroke();
|
|
ctx.fillStyle = 'rgba(255,224,138,0.08)'; ctx.fill();
|
|
}
|
|
|
|
ctx.globalAlpha = off ? 0.72 : 1;
|
|
|
|
if (!b.done) { drawConstruction(b, timeSec); ctx.globalAlpha = 1; return; }
|
|
|
|
const WALL = '#c9b18a', WALL_D = '#a08a66', WALL_R = '#b59a74';
|
|
|
|
switch (b.defId) {
|
|
case 'hq': {
|
|
isoBox(b.x, b.y, hf, 34, '#b8a07a', '#8f7a58', '#a58c66');
|
|
isoBox(b.x, b.y - 0.4, hf * 0.55, 26, '#a5502e', '#7a3b22', '#8f4428', 34);
|
|
// banners
|
|
const p = S.worldToScreen(b.x - hf + 0.25, b.y - hf + 0.25);
|
|
const z = S.cam.zoom;
|
|
ctx.strokeStyle = '#5d452c'; ctx.lineWidth = 2 * z;
|
|
ctx.beginPath(); ctx.moveTo(p.x, p.y - 60 * z); ctx.lineTo(p.x, p.y - 100 * z); ctx.stroke();
|
|
const wav = Math.sin(timeSec * 4) * 3 * z;
|
|
ctx.fillStyle = '#c23b2e';
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x, p.y - 100 * z);
|
|
ctx.quadraticCurveTo(p.x + 14 * z, p.y - 97 * z + wav, p.x + 26 * z, p.y - 94 * z);
|
|
ctx.quadraticCurveTo(p.x + 13 * z, p.y - 91 * z - wav, p.x, p.y - 88 * z);
|
|
ctx.closePath(); ctx.fill();
|
|
break;
|
|
}
|
|
case 'house': {
|
|
isoBox(b.x, b.y, hf, 16, WALL, WALL_D, WALL_R);
|
|
// gable roof
|
|
const z = S.cam.zoom;
|
|
const pA = S.worldToScreen(b.x - hf, b.y - hf), pB = S.worldToScreen(b.x + hf, b.y - hf);
|
|
const pC = S.worldToScreen(b.x + hf, b.y + hf), pD = S.worldToScreen(b.x - hf, b.y + hf);
|
|
const hh = 30 * z, mh = 40 * z;
|
|
ctx.fillStyle = '#a5502e';
|
|
ctx.beginPath();
|
|
ctx.moveTo(pA.x, pA.y - hh); ctx.lineTo((pA.x + pB.x) / 2, (pA.y + pB.y) / 2 - mh);
|
|
ctx.lineTo(pB.x, pB.y - hh); ctx.lineTo(pB.x, pB.y - 16 * z);
|
|
ctx.lineTo(pA.x, pA.y - 16 * z);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = '#8f4428';
|
|
ctx.beginPath();
|
|
ctx.moveTo(pB.x, pB.y - hh); ctx.lineTo((pB.x + pC.x) / 2, (pB.y + pC.y) / 2 - mh);
|
|
ctx.lineTo(pC.x, pC.y - hh); ctx.lineTo(pC.x, pC.y - 16 * z);
|
|
ctx.lineTo(pB.x, pB.y - 16 * z);
|
|
ctx.closePath(); ctx.fill();
|
|
break;
|
|
}
|
|
case 'farm': {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom, hw = hf * TW2 * z, hh = hf * TH2 * z;
|
|
diamond(p.x, p.y, hw, hh);
|
|
ctx.fillStyle = '#7a5f38'; ctx.fill();
|
|
ctx.save();
|
|
diamond(p.x, p.y, hw * 0.92, hh * 0.92);
|
|
ctx.clip();
|
|
for (let k = -4; k <= 4; k++) {
|
|
ctx.strokeStyle = k % 2 ? '#8fae4a' : '#6f9440';
|
|
ctx.lineWidth = 3.4 * z;
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x - hw + k * hw / 4.4 - hw * 0.2, p.y + k * hh / 4.4);
|
|
ctx.lineTo(p.x + k * hw / 4.4 + hw * 0.2, p.y + hh + k * hh / 4.4 - hh * 2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
// fence posts
|
|
ctx.fillStyle = '#6b5233';
|
|
for (const [fx, fy] of [[-1, -1], [1, -1], [1, 1], [-1, 1], [0, -1], [1, 0], [0, 1], [-1, 0]]) {
|
|
const fp = S.worldToScreen(b.x + fx * hf * 0.98, b.y + fy * hf * 0.98);
|
|
ctx.fillRect(fp.x - 1 * z, fp.y - 7 * z, 2 * z, 7 * z);
|
|
}
|
|
// windmill
|
|
const mp = S.worldToScreen(b.x - hf + 0.35, b.y - hf + 0.35);
|
|
ctx.strokeStyle = '#5d452c'; ctx.lineWidth = 2.5 * z;
|
|
ctx.beginPath(); ctx.moveTo(mp.x, mp.y); ctx.lineTo(mp.x, mp.y - 34 * z); ctx.stroke();
|
|
const rot = timeSec * 2.4;
|
|
ctx.save();
|
|
ctx.translate(mp.x, mp.y - 34 * z);
|
|
for (let k = 0; k < 4; k++) {
|
|
ctx.rotate(Math.PI / 2);
|
|
ctx.fillStyle = '#cfc5a8';
|
|
ctx.fillRect(0, -1.2 * z, 13 * z, 2.4 * z);
|
|
}
|
|
ctx.restore();
|
|
break;
|
|
}
|
|
case 'forester': {
|
|
isoBox(b.x, b.y, hf, 15, '#8a6842', '#6b5033', '#7a5c3a');
|
|
// log pile
|
|
const lp = S.worldToScreen(b.x + hf - 0.3, b.y + hf - 0.3);
|
|
const z = S.cam.zoom;
|
|
for (let k = 0; k < 3; k++) {
|
|
ctx.fillStyle = '#5d452c';
|
|
ctx.beginPath(); ctx.ellipse(lp.x - k * 7 * z, lp.y - 3 * z, 3.4 * z, 2.6 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#c9a86a';
|
|
ctx.beginPath(); ctx.ellipse(lp.x - k * 7 * z, lp.y - 3 * z, 1.6 * z, 1.2 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
break;
|
|
}
|
|
case 'quarry': {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom, hw = hf * TW2 * z, hh = hf * TH2 * z;
|
|
diamond(p.x, p.y, hw, hh);
|
|
ctx.fillStyle = '#8d8d90'; ctx.fill();
|
|
diamond(p.x, p.y, hw * 0.6, hh * 0.6);
|
|
ctx.fillStyle = '#77777c'; ctx.fill();
|
|
ctx.fillStyle = '#a5abb2';
|
|
for (const [ox, oy, rr] of [[-0.4, -0.2, 5], [0.3, 0.1, 4], [0, 0.45, 3.4]]) {
|
|
const rp = S.worldToScreen(b.x + ox, b.y + oy);
|
|
ctx.beginPath();
|
|
ctx.moveTo(rp.x - rr * z, rp.y);
|
|
ctx.lineTo(rp.x - rr * 0.4 * z, rp.y - rr * 1.3 * z);
|
|
ctx.lineTo(rp.x + rr * 0.6 * z, rp.y - rr * 1.1 * z);
|
|
ctx.lineTo(rp.x + rr * z, rp.y);
|
|
ctx.closePath(); ctx.fill();
|
|
}
|
|
// hoist
|
|
const hp2 = S.worldToScreen(b.x - hf + 0.4, b.y - hf + 0.4);
|
|
ctx.strokeStyle = '#5d452c'; ctx.lineWidth = 2 * z;
|
|
ctx.beginPath(); ctx.moveTo(hp2.x, hp2.y); ctx.lineTo(hp2.x, hp2.y - 26 * z); ctx.lineTo(hp2.x + 12 * z, hp2.y - 20 * z); ctx.stroke();
|
|
break;
|
|
}
|
|
case 'generator': {
|
|
isoBox(b.x, b.y, hf, 14, '#7f8a93', '#5f6a73', '#6e7982');
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom;
|
|
ctx.strokeStyle = '#3d464e'; ctx.lineWidth = 2 * z;
|
|
ctx.beginPath(); ctx.moveTo(p.x, p.y - 14 * z); ctx.lineTo(p.x, p.y - 30 * z); ctx.stroke();
|
|
ctx.fillStyle = '#ffd76a';
|
|
ctx.beginPath(); ctx.arc(p.x, p.y - 33 * z, 3.4 * z, 0, Math.PI * 2); ctx.fill();
|
|
if (!off && Math.random() < 0.15) {
|
|
ctx.strokeStyle = 'rgba(255,230,120,0.8)';
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x, p.y - 33 * z);
|
|
ctx.lineTo(p.x + (Math.random() - 0.5) * 10 * z, p.y - 33 * z + (Math.random() - 0.5) * 10 * z);
|
|
ctx.stroke();
|
|
}
|
|
break;
|
|
}
|
|
case 'barracks': {
|
|
isoBox(b.x, b.y, hf, 18, '#8f8577', '#6f665a', '#7d7365');
|
|
// pitched roof
|
|
const z = S.cam.zoom;
|
|
const pA = S.worldToScreen(b.x - hf, b.y - hf), pB = S.worldToScreen(b.x + hf, b.y - hf);
|
|
const pC = S.worldToScreen(b.x + hf, b.y + hf), pD = S.worldToScreen(b.x - hf, b.y + hf);
|
|
const hh2 = 34 * z;
|
|
ctx.fillStyle = '#5d6a52';
|
|
ctx.beginPath();
|
|
ctx.moveTo(pA.x, pA.y - 18 * z); ctx.lineTo(pB.x, pB.y - 18 * z); ctx.lineTo(pC.x, pC.y - hh2); ctx.lineTo(pD.x, pD.y - hh2);
|
|
ctx.closePath(); ctx.fill();
|
|
// flag
|
|
const fp = S.worldToScreen(b.x, b.y);
|
|
ctx.strokeStyle = '#3d3630'; ctx.lineWidth = 1.6 * z;
|
|
ctx.beginPath(); ctx.moveTo(fp.x, fp.y - 34 * z); ctx.lineTo(fp.x, fp.y - 48 * z); ctx.stroke();
|
|
ctx.fillStyle = '#3f6d3f';
|
|
const wav = Math.sin(timeSec * 5) * 2 * z;
|
|
ctx.beginPath(); ctx.moveTo(fp.x, fp.y - 48 * z); ctx.lineTo(fp.x + 12 * z, fp.y - 45 * z + wav); ctx.lineTo(fp.x, fp.y - 42 * z); ctx.closePath(); ctx.fill();
|
|
break;
|
|
}
|
|
case 'watchtower': {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
|
ctx.beginPath(); ctx.ellipse(p.x, p.y + 1, 12 * z, 5 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.strokeStyle = '#6b5033'; ctx.lineWidth = 3 * z;
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x - 8 * z, p.y); ctx.lineTo(p.x - 3 * z, p.y - 34 * z);
|
|
ctx.moveTo(p.x + 8 * z, p.y); ctx.lineTo(p.x + 3 * z, p.y - 34 * z);
|
|
ctx.stroke();
|
|
// cabin
|
|
isoBox(b.x, b.y, 0.32, 10, '#8a6842', '#6b5033', '#7a5c3a', 34);
|
|
// roof
|
|
ctx.fillStyle = '#544a3a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x - 14 * z, p.y - 44 * z); ctx.lineTo(p.x, p.y - 54 * z); ctx.lineTo(p.x + 14 * z, p.y - 44 * z);
|
|
ctx.closePath(); ctx.fill();
|
|
// gunner glint
|
|
if (b.cool > def.rof - 0.1) {
|
|
ctx.fillStyle = '#fff3c0';
|
|
ctx.beginPath(); ctx.arc(p.x + 6 * z, p.y - 38 * z, 2 * z, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
break;
|
|
}
|
|
case 'cannon': {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom;
|
|
diamond(p.x, p.y, TW2 * z * 0.8, TH2 * z * 0.8);
|
|
ctx.fillStyle = '#8d8d90'; ctx.fill();
|
|
ctx.strokeStyle = '#5f5f63'; ctx.stroke();
|
|
// barrel rotates
|
|
const ang = b.aimAng != null ? b.aimAng : -Math.PI / 4;
|
|
ctx.save();
|
|
ctx.translate(p.x, p.y - 10 * z);
|
|
ctx.fillStyle = '#3d4147';
|
|
ctx.beginPath(); ctx.arc(0, 0, 7 * z, 0, Math.PI * 2); ctx.fill();
|
|
ctx.rotate(ang);
|
|
ctx.fillStyle = '#2f3338';
|
|
ctx.fillRect(0, -3 * z, 20 * z, 6 * z);
|
|
ctx.restore();
|
|
break;
|
|
}
|
|
case 'primalpen': {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom;
|
|
const hw = hf * TW2 * z, hh = hf * TH2 * z;
|
|
// dirt paddock
|
|
diamond(p.x, p.y, hw, hh);
|
|
ctx.fillStyle = '#6e5a3c'; ctx.fill();
|
|
diamond(p.x, p.y, hw * 0.82, hh * 0.82);
|
|
ctx.fillStyle = '#82704c'; ctx.fill();
|
|
// fence rails + posts all around
|
|
const corners = [[-hf, -hf], [hf, -hf], [hf, hf], [-hf, hf]];
|
|
for (let k = 0; k < 4; k++) {
|
|
const a = corners[k], c = corners[(k + 1) % 4];
|
|
const pa = S.worldToScreen(b.x + a[0] * 0.95, b.y + a[1] * 0.95);
|
|
const pc = S.worldToScreen(b.x + c[0] * 0.95, b.y + c[1] * 0.95);
|
|
ctx.strokeStyle = '#6b5033'; ctx.lineWidth = 1.6 * z;
|
|
ctx.beginPath(); ctx.moveTo(pa.x, pa.y - 12 * z); ctx.lineTo(pc.x, pc.y - 12 * z); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(pa.x, pa.y - 6 * z); ctx.lineTo(pc.x, pc.y - 6 * z); ctx.stroke();
|
|
ctx.fillStyle = '#5d452c';
|
|
ctx.fillRect(pa.x - 1.5 * z, pa.y - 14 * z, 3 * z, 14 * z);
|
|
}
|
|
// feeding trough + hay
|
|
const tp = S.worldToScreen(b.x + 0.45, b.y - 0.45);
|
|
ctx.fillStyle = '#5d452c';
|
|
ctx.fillRect(tp.x - 7 * z, tp.y - 5 * z, 14 * z, 4 * z);
|
|
ctx.fillStyle = '#c9a86a';
|
|
ctx.beginPath(); ctx.ellipse(tp.x, tp.y - 5 * z, 6 * z, 2.2 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
// skull totem
|
|
const sp = S.worldToScreen(b.x - hf + 0.15, b.y - hf + 0.15);
|
|
ctx.strokeStyle = '#5d452c'; ctx.lineWidth = 2 * z;
|
|
ctx.beginPath(); ctx.moveTo(sp.x, sp.y); ctx.lineTo(sp.x, sp.y - 20 * z); ctx.stroke();
|
|
ctx.fillStyle = '#e8dcc0';
|
|
ctx.beginPath(); ctx.arc(sp.x, sp.y - 23 * z, 4 * z, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#3d3630';
|
|
ctx.beginPath(); ctx.arc(sp.x - 1.4 * z, sp.y - 24 * z, 1 * z, 0, Math.PI * 2); ctx.fill();
|
|
break;
|
|
}
|
|
case 'wall':
|
|
case 'gate': {
|
|
isoBox(b.x, b.y, 0.46, 15, '#9aa0a6', '#767c82', '#878d93');
|
|
if (b.defId === 'gate') {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom;
|
|
ctx.fillStyle = '#4a3b28';
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y - 6 * z, 6 * z, Math.PI, 0);
|
|
ctx.rect(p.x - 6 * z, p.y - 6 * z, 12 * z, 7 * z);
|
|
ctx.fill();
|
|
} else {
|
|
const p = S.worldToScreen(b.x, b.y - 0.0);
|
|
const z = S.cam.zoom;
|
|
ctx.fillStyle = 'rgba(255,255,255,0.13)';
|
|
ctx.fillRect(p.x - 10 * z, p.y - 15 * z, 20 * z, 2 * z);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// disabled bolt
|
|
if (off) {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
const z = S.cam.zoom;
|
|
ctx.font = (14 * z) + 'px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillStyle = '#ffd76a';
|
|
ctx.fillText('⚡', p.x, p.y - 40 * z + Math.sin(timeSec * 3) * 2 * z);
|
|
}
|
|
// burn smoke handled by sim
|
|
ctx.globalAlpha = 1;
|
|
|
|
// hp bar
|
|
if (b.hp < b.maxHp) drawBar(b.x, b.y, b.size * 0.5, b.hp / b.maxHp, '#5fbf5f');
|
|
}
|
|
|
|
function drawConstruction(b, timeSec) {
|
|
const hf = b.size / 2 - 0.05;
|
|
const z = S.cam.zoom;
|
|
const pC = S.worldToScreen(b.x, b.y);
|
|
// dirt pad
|
|
diamond(pC.x, pC.y, hf * TW2 * z, hf * TH2 * z);
|
|
ctx.fillStyle = '#7a6a4a'; ctx.fill();
|
|
// scaffold poles
|
|
const corners = [[-1, -1], [1, -1], [1, 1], [-1, 1]];
|
|
ctx.strokeStyle = '#8a6842'; ctx.lineWidth = 2 * z;
|
|
for (const [cx2, cy2] of corners) {
|
|
const p = S.worldToScreen(b.x + cx2 * hf, b.y + cy2 * hf);
|
|
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x, p.y - 26 * z); ctx.stroke();
|
|
}
|
|
// beams rise with progress
|
|
const ph = 26 * z * b.progress;
|
|
ctx.beginPath();
|
|
const pa = S.worldToScreen(b.x - hf, b.y - hf), pb = S.worldToScreen(b.x + hf, b.y - hf);
|
|
ctx.moveTo(pa.x, pa.y - ph); ctx.lineTo(pb.x, pb.y - ph);
|
|
ctx.stroke();
|
|
// materials pile
|
|
ctx.fillStyle = '#a58c66';
|
|
ctx.beginPath(); ctx.ellipse(pC.x + 8 * z, pC.y + 2 * z, 6 * z, 3 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
drawBar(b.x, b.y, b.size * 0.5, b.progress, '#e0b45c');
|
|
}
|
|
|
|
function drawBar(wx, wy, halfTiles, frac, colFront) {
|
|
const p = S.worldToScreen(wx, wy - halfTiles);
|
|
const z = S.cam.zoom;
|
|
const w = Math.max(26 * z, halfTiles * 2 * TW2 * z * 0.7), h = 4 * z;
|
|
const y = p.y - 8 * z;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.55)';
|
|
ctx.fillRect(p.x - w / 2 - 1, y - 1, w + 2, h + 2);
|
|
ctx.fillStyle = colFront;
|
|
ctx.fillRect(p.x - w / 2, y, w * U.clamp(frac, 0, 1), h);
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawUnit(u, timeSec) {
|
|
const p = S.worldToScreen(u.x, u.y);
|
|
const z = S.cam.zoom;
|
|
const bob = Math.sin(u.animT * 9) * 1.2 * z;
|
|
const bx = p.x, by = p.y - 6 * z + bob * 0.4;
|
|
const isRanger = u.unitId === 'ranger';
|
|
const isTamer = u.unitId === 'tamer';
|
|
// shadow
|
|
ctx.fillStyle = 'rgba(0,0,0,0.25)';
|
|
ctx.beginPath(); ctx.ellipse(p.x, p.y + 1 * z, 5 * z, 2.4 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
if (u.selected) {
|
|
ctx.strokeStyle = '#cfe8ff'; ctx.lineWidth = 1.5 * z;
|
|
ctx.beginPath(); ctx.ellipse(p.x, p.y + 1 * z, 7 * z, 3.4 * z, 0, 0, Math.PI * 2); ctx.stroke();
|
|
}
|
|
// legs
|
|
ctx.strokeStyle = '#3d3630'; ctx.lineWidth = 1.6 * z;
|
|
const lg = Math.sin(u.animT * 11) * 2.4 * z;
|
|
ctx.beginPath();
|
|
ctx.moveTo(bx, by + 2 * z); ctx.lineTo(bx - lg, p.y);
|
|
ctx.moveTo(bx, by + 2 * z); ctx.lineTo(bx + lg, p.y);
|
|
ctx.stroke();
|
|
// torso
|
|
ctx.fillStyle = isTamer ? '#6b4a8a' : (isRanger ? '#3f6d3f' : '#8a6d4a');
|
|
ctx.beginPath();
|
|
ctx.ellipse(bx, by, 2.6 * z, 4 * z, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// head
|
|
ctx.fillStyle = '#e0b48a';
|
|
ctx.beginPath(); ctx.arc(bx, by - 5.4 * z, 2.4 * z, 0, Math.PI * 2); ctx.fill();
|
|
// hat
|
|
ctx.fillStyle = isTamer ? '#4a3266' : (isRanger ? '#2f4f33' : '#6b5233');
|
|
ctx.beginPath(); ctx.ellipse(bx, by - 6.6 * z, isTamer ? 3.6 * z : 3 * z, isTamer ? 1.1 * z : 1.4 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
// rifle / tranq gun
|
|
const fa = u.facing;
|
|
const dxs = (Math.cos(fa) - Math.sin(fa)) * TW2, dys = (Math.cos(fa) + Math.sin(fa)) * TH2;
|
|
const al = Math.atan2(dys, dxs);
|
|
ctx.strokeStyle = isTamer ? '#5a4472' : '#2c2c2c'; ctx.lineWidth = 1.6 * z;
|
|
ctx.beginPath();
|
|
ctx.moveTo(bx + Math.cos(al) * 2 * z, by - 1.5 * z + Math.sin(al) * 1 * z);
|
|
ctx.lineTo(bx + Math.cos(al) * 9 * z, by - 1.5 * z + Math.sin(al) * 4.5 * z);
|
|
ctx.stroke();
|
|
// capture rope to the dino being collared
|
|
if (u.channelId) {
|
|
const tgt = RTS.sim.getDino(u.channelId);
|
|
if (tgt) {
|
|
const tp2 = S.worldToScreen(tgt.x, tgt.y);
|
|
ctx.strokeStyle = 'rgba(176,111,224,0.85)';
|
|
ctx.setLineDash([3, 3]);
|
|
ctx.lineWidth = 1.2 * z;
|
|
ctx.beginPath();
|
|
ctx.moveTo(bx + 3 * z, by - 2 * z);
|
|
ctx.lineTo(tp2.x, tp2.y - 10 * z);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
}
|
|
// hp pip
|
|
if (u.hp < u.maxHp) drawBar(u.x, u.y, 0.2, u.hp / u.maxHp, '#5fbf5f');
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// nest with incubating egg(s); wobbles harder as hatching nears
|
|
function drawEgg(e, timeSec) {
|
|
const p = S.worldToScreen(e.x, e.y);
|
|
const z = S.cam.zoom;
|
|
const frac = U.clamp(e.t / e.total, 0, 1);
|
|
// nest ring
|
|
ctx.fillStyle = '#6b5033';
|
|
ctx.beginPath(); ctx.ellipse(p.x, p.y, 9 * z, 4 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#8a6842';
|
|
ctx.beginPath(); ctx.ellipse(p.x, p.y - 1 * z, 7.5 * z, 3.2 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
// eggs (two, second appears halfway)
|
|
const wob = Math.sin(timeSec * (2 + frac * 14)) * frac; // shakes before hatching
|
|
for (let k = 0; k < (frac > 0.5 ? 2 : 1); k++) {
|
|
const ox = (k === 0 ? -2.5 : 2) * z + wob * 2 * z;
|
|
const oy = -3 * z - k * 1.5 * z;
|
|
ctx.fillStyle = '#efe6d2';
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x + ox, p.y + oy, 2.6 * z, 3.4 * z, wob * 0.25, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = 'rgba(120,100,80,0.35)';
|
|
ctx.beginPath(); ctx.ellipse(p.x + ox - 0.8 * z, p.y + oy - 1 * z, 0.9 * z, 1.2 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
// cracks near hatch
|
|
if (frac > 0.75) {
|
|
ctx.strokeStyle = '#5d452c'; ctx.lineWidth = 1 * z;
|
|
ctx.beginPath();
|
|
const ex = p.x - 2.5 * z + wob * 2 * z, ey = p.y - 3 * z;
|
|
ctx.moveTo(ex - 1.5 * z, ey);
|
|
ctx.lineTo(ex - 0.5 * z, ey + 1.5 * z);
|
|
ctx.lineTo(ex + 0.8 * z, ey - 0.5 * z);
|
|
ctx.stroke();
|
|
}
|
|
// progress pip
|
|
if (frac < 1) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
ctx.fillRect(p.x - 8 * z, p.y - 10 * z, 16 * z, 2.5 * z);
|
|
ctx.fillStyle = '#ffe9a8';
|
|
ctx.fillRect(p.x - 8 * z, p.y - 10 * z, 16 * z * frac, 2.5 * z);
|
|
}
|
|
}
|
|
|
|
function drawDino(d, timeSec) {
|
|
const p = S.worldToScreen(d.x, d.y);
|
|
const z = S.cam.zoom;
|
|
const sc = d.scale * z * (d.baby ? U.lerp(0.55, 1, d.growth || 0) : 1);
|
|
const flyZ = d.flying ? (2.2 + Math.sin(timeSec * 3 + d.animT) * 0.35) : 0;
|
|
// swimming? (amphibious dinos standing on water)
|
|
const stt = RTS.sim.state();
|
|
let inWater = false;
|
|
if (d.amphibious) {
|
|
const ti = (d.y | 0) * W + (d.x | 0);
|
|
inWater = U.inBounds(d.x | 0, d.y | 0, W, H) && stt.world.tiles.terrain[ti] === 3;
|
|
}
|
|
const swimZ = inWater ? -1.2 : 0;
|
|
// shadow
|
|
ctx.fillStyle = d.flying ? 'rgba(0,0,0,0.14)' : 'rgba(0,0,0,0.25)';
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y + 1 * z, 10 * sc, 4.4 * sc, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
if (inWater) {
|
|
// ripples
|
|
const rr = (timeSec * 1.6 + d.animT) % 1;
|
|
ctx.strokeStyle = 'rgba(200,230,255,' + (0.4 * (1 - rr)) + ')';
|
|
ctx.lineWidth = 1.4 * z;
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y, (8 + 14 * rr) * sc, (3.5 + 6 * rr) * sc, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// orientation: world facing -> screen angle
|
|
const fx = Math.cos(d.facing), fy = Math.sin(d.facing);
|
|
const ang = Math.atan2((fx + fy) * TH2, (fx - fy) * TW2);
|
|
|
|
ctx.save();
|
|
ctx.translate(p.x, p.y - (flyZ * 22 + swimZ * 8) * z);
|
|
ctx.rotate(ang);
|
|
ctx.scale(sc, sc);
|
|
|
|
const bodyCol = d.hitFlash > 0 ? '#ffffff' : d.color;
|
|
const darkCol = d.hitFlash > 0 ? '#ffffff' : shade(d.color, -34);
|
|
const legSwing = Math.sin(d.animT * 10) * 2.6;
|
|
|
|
if (d.flying) {
|
|
// wings
|
|
const flap = Math.sin(timeSec * 11 + d.animT) * 0.9;
|
|
ctx.fillStyle = darkCol;
|
|
for (const sgn of [-1, 1]) {
|
|
ctx.save();
|
|
ctx.scale(1, sgn);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-2, -1);
|
|
ctx.quadraticCurveTo(6, -10 * (0.6 + flap * 0.4), 16, -7 * (0.5 + flap));
|
|
ctx.quadraticCurveTo(8, -2, -2, 2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
// body + head + crest
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath(); ctx.ellipse(0, 0, 8, 3, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.beginPath(); ctx.ellipse(8.5, -2, 3.4, 2, 0.3, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = darkCol;
|
|
ctx.beginPath();
|
|
ctx.moveTo(9.5, -3.5); ctx.lineTo(14, -6.5); ctx.lineTo(11.5, -1.5);
|
|
ctx.closePath(); ctx.fill();
|
|
// beak
|
|
ctx.fillStyle = '#d8a03f';
|
|
ctx.beginPath(); ctx.moveTo(11, -1.6); ctx.lineTo(16, -0.4); ctx.lineTo(10.8, 0.4); ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = '#111';
|
|
ctx.beginPath(); ctx.arc(9.8, -2.6, 0.7, 0, Math.PI * 2); ctx.fill();
|
|
} else {
|
|
const big = d.dinoId === 'rex' || d.dinoId === 'trike' || d.dinoId === 'sucho';
|
|
const L = big ? 13 : 9; // body length
|
|
const HH = big ? 6 : 3.8; // body height
|
|
// far legs (hidden while swimming)
|
|
if (!inWater) {
|
|
ctx.strokeStyle = darkCol; ctx.lineWidth = big ? 3 : 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(L * 0.45, 0); ctx.lineTo(L * 0.45 + legSwing * 0.6, HH + 3);
|
|
ctx.moveTo(-L * 0.5, 1); ctx.lineTo(-L * 0.5 - legSwing * 0.6, HH + 3);
|
|
ctx.stroke();
|
|
}
|
|
// tail
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-L * 0.7, -HH * 0.5);
|
|
ctx.quadraticCurveTo(-L * 1.5, -HH * 0.2 + Math.sin(d.animT * 6) * 2, -L * 2.1, HH * 0.5 + Math.sin(d.animT * 6) * 2.5);
|
|
ctx.quadraticCurveTo(-L * 1.4, HH * 0.6, -L * 0.7, HH * 0.5);
|
|
ctx.closePath(); ctx.fill();
|
|
// body
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, L, HH, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
// belly stripe
|
|
ctx.fillStyle = darkCol;
|
|
ctx.globalAlpha = 0.35;
|
|
ctx.beginPath(); ctx.ellipse(0, HH * 0.45, L * 0.8, HH * 0.4, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
// near legs (hidden while swimming)
|
|
if (!inWater) {
|
|
ctx.strokeStyle = bodyCol;
|
|
ctx.lineWidth = big ? 3.4 : 2.2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(L * 0.4, 1); ctx.lineTo(L * 0.4 - legSwing * 0.7, HH + 3.5);
|
|
ctx.moveTo(-L * 0.45, 1.5); ctx.lineTo(-L * 0.45 + legSwing * 0.7, HH + 3.5);
|
|
ctx.stroke();
|
|
} else {
|
|
// paddle churn
|
|
ctx.strokeStyle = 'rgba(220,240,255,0.5)';
|
|
ctx.lineWidth = 1.4;
|
|
const churn = Math.sin(timeSec * 6 + d.animT) * 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(L * 0.4, 1); ctx.lineTo(L * 0.4 + churn, HH * 0.6);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// neck + head (species specific)
|
|
const nx = L * 0.85, ny = -HH * 0.9;
|
|
if (d.dinoId === 'trike') {
|
|
// frill
|
|
ctx.fillStyle = darkCol;
|
|
ctx.beginPath();
|
|
ctx.arc(nx - 1, ny - 1, HH * 1.7, Math.PI * 0.75, Math.PI * 1.75);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath(); ctx.ellipse(nx + 2, ny, HH * 0.95, HH * 0.75, 0, 0, Math.PI * 2); ctx.fill();
|
|
// horns
|
|
ctx.strokeStyle = '#e8dcc0'; ctx.lineWidth = 1.6;
|
|
ctx.beginPath();
|
|
ctx.moveTo(nx + 3, ny - 2); ctx.lineTo(nx + 8, ny - 6);
|
|
ctx.moveTo(nx + 1, ny - 3.5); ctx.lineTo(nx + 4, ny - 8);
|
|
ctx.stroke();
|
|
} else if (d.dinoId === 'sucho') {
|
|
// long crocodilian neck & snout
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath();
|
|
ctx.moveTo(L * 0.45, -HH * 0.8);
|
|
ctx.quadraticCurveTo(nx - 2, ny - 7, nx + 3, ny - 5);
|
|
ctx.quadraticCurveTo(nx, ny, L * 0.55, 0);
|
|
ctx.closePath(); ctx.fill();
|
|
// head + long jaws
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath(); ctx.ellipse(nx + 4, ny - 4, 6.5, 2.4, 0.08, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#e8e0c8';
|
|
for (let k = 0; k < 5; k++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(nx + k * 2.2, ny - 2.2);
|
|
ctx.lineTo(nx + k * 2.2 + 1, ny - 0.6);
|
|
ctx.lineTo(nx + k * 2.2 + 2, ny - 2.2);
|
|
ctx.closePath(); ctx.fill();
|
|
}
|
|
// back sail
|
|
ctx.fillStyle = darkCol;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-L * 0.55, -HH * 0.75);
|
|
ctx.quadraticCurveTo(-L * 0.1, -HH * 2.6, L * 0.35, -HH * 0.85);
|
|
ctx.quadraticCurveTo(-L * 0.1, -HH * 1.15, -L * 0.55, -HH * 0.75);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = '#111';
|
|
ctx.beginPath(); ctx.arc(nx + 3, ny - 5.4, 0.9, 0, Math.PI * 2); ctx.fill();
|
|
} else if (d.dinoId === 'rex') {
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath();
|
|
ctx.moveTo(L * 0.5, -HH);
|
|
ctx.quadraticCurveTo(nx, ny - 8, nx + 5, ny - 6);
|
|
ctx.quadraticCurveTo(nx + 2, ny - 1, L * 0.7, 0);
|
|
ctx.closePath(); ctx.fill();
|
|
// head w/ jaw + teeth
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath(); ctx.ellipse(nx + 6, ny - 5, 5.5, 3.4, 0.15, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#fff';
|
|
let tx0 = nx + 3;
|
|
for (let k = 0; k < 4; k++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(tx0 + k * 2.2, ny - 2.4);
|
|
ctx.lineTo(tx0 + k * 2.2 + 1, ny);
|
|
ctx.lineTo(tx0 + k * 2.2 + 2, ny - 2.4);
|
|
ctx.closePath(); ctx.fill();
|
|
}
|
|
ctx.fillStyle = '#111';
|
|
ctx.beginPath(); ctx.arc(nx + 5, ny - 7, 0.9, 0, Math.PI * 2); ctx.fill();
|
|
// tiny arms
|
|
ctx.strokeStyle = darkCol; ctx.lineWidth = 1.4;
|
|
ctx.beginPath(); ctx.moveTo(L * 0.55, -HH * 0.3); ctx.lineTo(L * 0.75, HH * 0.3); ctx.stroke();
|
|
} else if (d.dinoId === 'dilo') {
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath();
|
|
ctx.moveTo(L * 0.55, -HH * 0.8);
|
|
ctx.quadraticCurveTo(nx, ny - 6, nx + 4, ny - 4);
|
|
ctx.quadraticCurveTo(nx, ny, L * 0.6, 0);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.beginPath(); ctx.ellipse(nx + 4, ny - 3, 3.4, 2.2, 0.1, 0, Math.PI * 2); ctx.fill();
|
|
// neck frill
|
|
ctx.fillStyle = '#c96a2a';
|
|
ctx.beginPath(); ctx.ellipse(nx - 2, ny - 2, 3, 4.4, -0.4, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#111';
|
|
ctx.beginPath(); ctx.arc(nx + 4.5, ny - 4.4, 0.7, 0, Math.PI * 2); ctx.fill();
|
|
} else { // raptor / compy
|
|
ctx.fillStyle = bodyCol;
|
|
ctx.beginPath();
|
|
ctx.moveTo(L * 0.55, -HH * 0.9);
|
|
ctx.quadraticCurveTo(nx, ny - 5, nx + 3.4, ny - 3);
|
|
ctx.quadraticCurveTo(nx, ny, L * 0.6, 0);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.beginPath(); ctx.ellipse(nx + 3, ny - 2.4, 2.8, 1.7, 0.1, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = '#c9a86a';
|
|
ctx.beginPath(); ctx.moveTo(nx + 5, ny - 2); ctx.lineTo(nx + 8, ny - 1); ctx.lineTo(nx + 5, ny - 0.6); ctx.closePath(); ctx.fill();
|
|
ctx.fillStyle = '#111';
|
|
ctx.beginPath(); ctx.arc(nx + 3.6, ny - 3.4, 0.65, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
}
|
|
ctx.restore();
|
|
|
|
// capture-in-progress bar
|
|
if (d.beingTamed > 0) {
|
|
const w = Math.max(26 * z, d.r * 3 * TW2 * z);
|
|
const yy = p.y - flyZ * 22 * z - (d.flying ? 34 : 20) * sc - 10 * z;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.6)';
|
|
ctx.fillRect(p.x - w / 2 - 1, yy - 1, w + 2, 5 * z + 2);
|
|
ctx.fillStyle = '#b06fe0';
|
|
ctx.fillRect(p.x - w / 2, yy, w * U.clamp(d.beingTamed, 0, 1), 5 * z);
|
|
ctx.font = 'bold ' + (8 * Math.max(z, 0.8)) + 'px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillStyle = '#e6c8ff';
|
|
ctx.fillText('TAMING…', p.x, yy - 3 * z);
|
|
}
|
|
|
|
// tamed collar marker
|
|
if (d.tamed) {
|
|
const cy = p.y - flyZ * 22 * z - (d.flying ? 30 : 18) * sc - 4 * z;
|
|
const pulse = 0.7 + Math.sin(timeSec * 5 + d.animT) * 0.3;
|
|
ctx.globalAlpha = pulse;
|
|
ctx.fillStyle = '#7fd6ff';
|
|
diamond(p.x, cy, 3.4 * z, 1.9 * z);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// hp bar
|
|
if (d.hp < d.maxHp || d.tamed) {
|
|
const w = Math.max(20 * z, d.r * 2.6 * TW2 * z);
|
|
const yy = p.y - (flyZ * 22 + swimZ * 8) * z - (d.flying ? 26 : 14) * sc - (d.beingTamed > 0 ? 16 : 6) * z;
|
|
ctx.fillStyle = 'rgba(0,0,0,0.55)';
|
|
ctx.fillRect(p.x - w / 2 - 1, yy - 1, w + 2, 4 * z + 2);
|
|
ctx.fillStyle = d.tamed ? '#5fbf5f' : (d.mode === 'final' ? '#ff5f4a' : '#d84f3f');
|
|
ctx.fillRect(p.x - w / 2, yy, w * U.clamp(d.hp / d.maxHp, 0, 1), 4 * z);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawProjectiles(st, timeSec) {
|
|
const z = S.cam.zoom;
|
|
for (const pr of st.projs) {
|
|
const p = S.worldToScreen(pr.x, pr.y);
|
|
const zh = (pr.zh || 0) * 22 * z;
|
|
if (pr.type === 'bullet') {
|
|
const pp = S.worldToScreen(pr.px != null ? pr.px : pr.x, pr.py != null ? pr.py : pr.y);
|
|
ctx.strokeStyle = '#ffe08a';
|
|
ctx.lineWidth = 1.6 * z;
|
|
ctx.beginPath();
|
|
ctx.moveTo(pp.x, pp.y - 8 * z);
|
|
ctx.lineTo(p.x, p.y - 8 * z);
|
|
ctx.stroke();
|
|
} else {
|
|
// lobbed shell/spit
|
|
ctx.fillStyle = 'rgba(0,0,0,0.2)';
|
|
ctx.beginPath(); ctx.ellipse(p.x, p.y, 2.4 * z, 1.2 * z, 0, 0, Math.PI * 2); ctx.fill();
|
|
ctx.fillStyle = pr.color;
|
|
ctx.beginPath(); ctx.arc(p.x, p.y - 8 * z - zh, pr.type === 'shell' ? 3 * z : 2.2 * z, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawParticles(st) {
|
|
const z = S.cam.zoom;
|
|
for (const pt of st.parts) {
|
|
const p = S.worldToScreen(pt.x, pt.y);
|
|
const a = U.clamp(pt.life / pt.maxLife, 0, 1);
|
|
ctx.globalAlpha = a * (pt.type === 'smoke' ? 0.5 : 0.95);
|
|
ctx.fillStyle = pt.color;
|
|
const sz = pt.size * z * (pt.type === 'smoke' ? (2 - a) : 1);
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y - pt.z * z, sz, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
function drawTexts(st) {
|
|
const z = S.cam.zoom;
|
|
ctx.textAlign = 'center';
|
|
for (const t of st.texts) {
|
|
const p = S.worldToScreen(t.x, t.y);
|
|
const a = U.clamp(t.life / t.maxLife, 0, 1);
|
|
ctx.globalAlpha = a;
|
|
ctx.font = 'bold ' + (11 * Math.max(z, 0.8)) + 'px system-ui';
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.7)';
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeText(t.str, p.x, p.y - 20 * z);
|
|
ctx.fillStyle = t.color;
|
|
ctx.fillText(t.str, p.x, p.y - 20 * z);
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawFog(st) {
|
|
const z = S.cam.zoom;
|
|
const c0 = S.screenToWorld(0, 0), c1 = S.screenToWorld(vw, 0);
|
|
const c2 = S.screenToWorld(0, vh), c3 = S.screenToWorld(vw, vh);
|
|
const minX = Math.max(0, Math.floor(Math.min(c0.x, c1.x, c2.x, c3.x)) - 1);
|
|
const maxX = Math.min(W - 1, Math.ceil(Math.max(c0.x, c1.x, c2.x, c3.x)) + 1);
|
|
const minY = Math.max(0, Math.floor(Math.min(c0.y, c1.y, c2.y, c3.y)) - 1);
|
|
const maxY = Math.min(H - 1, Math.ceil(Math.max(c0.y, c1.y, c2.y, c3.y)) + 2);
|
|
for (let y = minY; y <= maxY; y++) {
|
|
for (let x = minX; x <= maxX; x++) {
|
|
const v = st.vis[y * W + x];
|
|
if (v === 2) continue;
|
|
const p = S.worldToScreen(x + 0.5, y + 0.5);
|
|
if (p.x < -TW || p.x > vw + TW || p.y < -TH * 2 || p.y > vh + TH * 2) continue;
|
|
diamond(p.x, p.y, TW2 * z, TH2 * z);
|
|
ctx.fillStyle = v === 0 ? 'rgba(6,9,7,0.94)' : 'rgba(6,9,7,0.45)';
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawNightTint(st) {
|
|
const DAY = C.WORLD.DAY_LENGTH;
|
|
const ph = (st.time % DAY) / DAY; // 0 = dawn
|
|
let dark;
|
|
if (ph < 0.45) dark = 0;
|
|
else if (ph < 0.55) dark = (ph - 0.45) / 0.10;
|
|
else if (ph < 0.90) dark = 1;
|
|
else dark = 1 - (ph - 0.90) / 0.10;
|
|
dark *= 0.30;
|
|
if (dark <= 0.005) return;
|
|
ctx.fillStyle = 'rgba(14,22,52,' + dark.toFixed(3) + ')';
|
|
ctx.fillRect(0, 0, vw, vh);
|
|
// warm glows around powered buildings at night
|
|
ctx.globalCompositeOperation = 'lighter';
|
|
for (const b of st.buildings) {
|
|
if (b.dead || !b.done || !b.powered) continue;
|
|
const p2 = S.worldToScreen(b.x, b.y);
|
|
const r = (b.size * 26 + 18) * S.cam.zoom;
|
|
const g = ctx.createRadialGradient(p2.x, p2.y - 8 * S.cam.zoom, 2, p2.x, p2.y - 8 * S.cam.zoom, r);
|
|
g.addColorStop(0, 'rgba(255,190,90,' + (dark * 0.55).toFixed(3) + ')');
|
|
g.addColorStop(1, 'rgba(255,190,90,0)');
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath();
|
|
ctx.ellipse(p2.x, p2.y, r, r * 0.6, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
function drawPlacementGuides(st, ui) {
|
|
const def = C.BUILDINGS[ui.placing.defId];
|
|
const z = S.cam.zoom;
|
|
// power radii
|
|
ctx.strokeStyle = 'rgba(120,220,255,0.5)';
|
|
ctx.setLineDash([6, 6]);
|
|
for (const b of st.buildings) {
|
|
if (b.dead || !b.done) continue;
|
|
const d2 = C.BUILDINGS[b.defId];
|
|
if (!d2.radius) continue;
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y, d2.radius * TW2 * z, d2.radius * TH2 * z, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
ctx.setLineDash([]);
|
|
// work range for harvesters
|
|
if (def.range) {
|
|
const p = S.worldToScreen(ui.placing.x, ui.placing.y);
|
|
ctx.strokeStyle = def.id === 'forester' ? 'rgba(120,200,110,0.6)' : 'rgba(180,180,190,0.6)';
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y, def.range * TW2 * z, def.range * TH2 * z, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function drawGhost(st, ui) {
|
|
const def = C.BUILDINGS[ui.placing.defId];
|
|
const chk = RTS.sim.canPlace(ui.placing.defId, ui.placing.x, ui.placing.y);
|
|
const ok = chk.ok;
|
|
const hf = def.size / 2 - 0.05;
|
|
const p = S.worldToScreen(ui.placing.x, ui.placing.y);
|
|
const z = S.cam.zoom;
|
|
diamond(p.x, p.y, hf * TW2 * z, hf * TH2 * z);
|
|
ctx.fillStyle = ok ? 'rgba(120,255,120,0.3)' : 'rgba(255,80,80,0.35)';
|
|
ctx.fill();
|
|
ctx.strokeStyle = ok ? '#8f8' : '#f55';
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
if (!ok) {
|
|
ctx.font = (11 * Math.max(z, 0.8)) + 'px system-ui';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillStyle = '#ffb0a8';
|
|
ctx.strokeStyle = 'rgba(0,0,0,0.8)';
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeText(chk.why, p.x, p.y - 26 * z);
|
|
ctx.fillText(chk.why, p.x, p.y - 26 * z);
|
|
}
|
|
}
|
|
|
|
function drawSelectionOverlays(st, ui) {
|
|
const z = S.cam.zoom;
|
|
// tower ranges
|
|
if (ui.sel && ui.sel.kind === 'building') {
|
|
const b = RTS.sim.getBuilding(ui.sel.id);
|
|
if (b) {
|
|
const def = C.BUILDINGS[b.defId];
|
|
if (def.range) {
|
|
const p = S.worldToScreen(b.x, b.y);
|
|
ctx.strokeStyle = 'rgba(255,220,120,0.55)';
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.beginPath();
|
|
ctx.ellipse(p.x, p.y, playerRangeOf(def) * TW2 * z, playerRangeOf(def) * TH2 * z, 0, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
if (b.defId === 'barracks' && b.rallyX != null) {
|
|
const rp = S.worldToScreen(b.rallyX, b.rallyY);
|
|
ctx.strokeStyle = '#cfe8ff';
|
|
ctx.lineWidth = 1.5 * z;
|
|
ctx.beginPath();
|
|
ctx.moveTo(rp.x, rp.y); ctx.lineTo(rp.x, rp.y - 14 * z);
|
|
ctx.moveTo(rp.x, rp.y - 14 * z); ctx.lineTo(rp.x + 8 * z, rp.y - 10 * z);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
// drag select rect
|
|
if (ui.dragRect) {
|
|
const x0 = Math.min(ui.dragRect.x0, ui.dragRect.x1), x1 = Math.max(ui.dragRect.x0, ui.dragRect.x1);
|
|
const y0 = Math.min(ui.dragRect.y0, ui.dragRect.y1), y1 = Math.max(ui.dragRect.y0, ui.dragRect.y1);
|
|
ctx.strokeStyle = 'rgba(255,255,255,0.8)';
|
|
ctx.fillStyle = 'rgba(255,255,255,0.08)';
|
|
ctx.lineWidth = 1;
|
|
ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
|
|
ctx.strokeRect(x0, y0, x1 - x0, y1 - y0);
|
|
}
|
|
}
|
|
|
|
function playerRangeOf(def) {
|
|
const st = RTS.sim.state();
|
|
const rngBonus = 1 + 0.12 * st.upgrades.range;
|
|
return def.range * rngBonus;
|
|
}
|
|
|
|
function drawWarningArrows(st, timeSec) {
|
|
const pulse = 0.5 + Math.sin(timeSec * 6) * 0.5;
|
|
const ang = Math.atan2(st.warnDirY, st.warnDirX);
|
|
// project direction to screen space
|
|
const dxs = (Math.cos(ang) - Math.sin(ang)) * TW2;
|
|
const dys = (Math.cos(ang) + Math.sin(ang)) * TH2;
|
|
const sang = Math.atan2(dys, dxs);
|
|
const cx = vw / 2, cy = vh / 2;
|
|
const rad = Math.min(vw, vh) * 0.38;
|
|
const ax = cx + Math.cos(sang) * rad, ay = cy + Math.sin(sang) * rad * 0.7;
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.55 + pulse * 0.45;
|
|
ctx.translate(ax, ay);
|
|
ctx.rotate(sang);
|
|
ctx.fillStyle = '#ff4a3a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(26, 0); ctx.lineTo(-10, -16); ctx.lineTo(-4, 0); ctx.lineTo(-10, 16);
|
|
ctx.closePath(); ctx.fill();
|
|
ctx.restore();
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
// ---------------------------------------------------------
|
|
// MINIMAP
|
|
// ---------------------------------------------------------
|
|
S.drawMinimap = function (mmCtx, st, size) {
|
|
const s = size / W;
|
|
mmCtx.clearRect(0, 0, size, size);
|
|
const T = st.world.tiles;
|
|
for (let y = 0; y < H; y++) {
|
|
for (let x = 0; x < W; x++) {
|
|
const i = y * W + x;
|
|
let col;
|
|
if (st.vis[i] === 0) col = '#0a0d0b';
|
|
else {
|
|
const t = T.terrain[i];
|
|
col = t === 3 ? '#2c4d6e' : (t === 2 ? '#71624a' : '#49653a');
|
|
if (T.tree[i] > 0) col = '#33552e';
|
|
else if (T.rock[i] > 0) col = '#71767c';
|
|
}
|
|
mmCtx.fillStyle = col;
|
|
mmCtx.fillRect(x * s, y * s, Math.ceil(s), Math.ceil(s));
|
|
}
|
|
}
|
|
// buildings
|
|
for (const b of st.buildings) {
|
|
if (b.dead) continue;
|
|
mmCtx.fillStyle = b.id === st.hqId ? '#ffe08a' : '#7fd6ff';
|
|
mmCtx.fillRect(b.x * s - 1, b.y * s - 1, Math.max(2, b.size * s), Math.max(2, b.size * s));
|
|
}
|
|
// dinos
|
|
mmCtx.fillStyle = '#ff4a3a';
|
|
for (const d of st.dinos) {
|
|
if (d.dead) continue;
|
|
const ti = (d.y | 0) * W + (d.x | 0);
|
|
if (st.vis[ti] !== 2 && d.mode !== 'final') continue;
|
|
mmCtx.fillRect(d.x * s - 1, d.y * s - 1, 2, 2);
|
|
}
|
|
// view polygon
|
|
mmCtx.strokeStyle = 'rgba(255,255,255,0.7)';
|
|
mmCtx.lineWidth = 1;
|
|
mmCtx.beginPath();
|
|
const corners = [[0, 0], [vw, 0], [vw, vh], [0, vh]];
|
|
corners.forEach(([sx2, sy2], k) => {
|
|
const wp = RTS.render.screenToWorld(sx2, sy2);
|
|
if (k === 0) mmCtx.moveTo(wp.x * s, wp.y * s);
|
|
else mmCtx.lineTo(wp.x * s, wp.y * s);
|
|
});
|
|
mmCtx.closePath();
|
|
mmCtx.stroke();
|
|
};
|
|
|
|
return S;
|
|
})();
|