wren/val Public Public: anyone can read this; nobody can change it.
forked from benjivers/val
workbenchthis machine
Engine renderer: colour bodies by side, not only mate/not-mate (#24 item 3) (#43)
Requested-by: benjivers <[email protected]> Co-authored-by: benjivers <[email protected]>
11 files
changed +271
−10
src/engine/body.test.ts +5
| @@ -12,3 +12,8 @@ test("a well-formed body has no problems, and each missing or mistyped field is | ||
| 12 | 12 | expect(bodyProblems(null)).toEqual(["body is not an object"]); |
| 13 | 13 | expect(meProblems({ id: "a" })).toContain("me.ammo should be a number, is undefined"); |
| 14 | 14 | }); |
| 15 | ||
| 16 | test("`side` may be left out, but when it is sent it is a string", () => { | |
| 17 | expect(bodyProblems({ ...good, side: "north" })).toEqual([]); | |
| 18 | expect(bodyProblems({ ...good, side: 3 })).toEqual(["body.side should be a string, is 3"]); | |
| 19 | }); |
src/engine/body.ts +16 −3
| @@ -16,6 +16,12 @@ | ||
| 16 | 16 | * hp: number 0..100; a bar shows under 100 |
| 17 | 17 | * mate: boolean on the viewer's side |
| 18 | 18 | * |
| 19 | * and may have: | |
| 20 | * side: string which side it is on, for a game of three sides or | |
| 21 | * more; each non-mate side then draws in its own colour | |
| 22 | * (`static/engine/sides.js`). Leave it out and every | |
| 23 | * non-mate draws in the one enemy colour, as before. | |
| 24 | * | |
| 19 | 25 | * `snap.me` has, besides id, x, y, angle, alive and hp: |
| 20 | 26 | * ammo: number rounds in the magazine |
| 21 | 27 | * reloading: boolean mid-reload |
| @@ -34,6 +40,11 @@ const BODY: Record<string, Kind> = { | ||
| 34 | 40 | mate: "boolean", |
| 35 | 41 | }; |
| 36 | 42 | |
| 43 | /** Fields a body may leave out, but must type right when it sends them. */ | |
| 44 | const BODY_OPTIONAL: Record<string, Kind> = { | |
| 45 | side: "string", | |
| 46 | }; | |
| 47 | ||
| 37 | 48 | const ME: Record<string, Kind> = { |
| 38 | 49 | id: "string", |
| 39 | 50 | x: "number", |
| @@ -45,18 +56,20 @@ const ME: Record<string, Kind> = { | ||
| 45 | 56 | reloading: "boolean", |
| 46 | 57 | }; |
| 47 | 58 | |
| 48 | function problems(value: unknown, shape: Record<string, Kind>, label: string): string[] { | |
| 59 | function problems(value: unknown, shape: Record<string, Kind>, label: string, optional: Record<string, Kind> = {}): string[] { | |
| 49 | 60 | if (typeof value !== "object" || value === null) return [`${label} is not an object`]; |
| 50 | 61 | const out: string[] = []; |
| 51 | for (const [field, kind] of Object.entries(shape)) { | |
| 62 | const fields = [...Object.entries(shape), ...Object.entries(optional)]; | |
| 63 | for (const [field, kind] of fields) { | |
| 52 | 64 | const got = (value as Record<string, unknown>)[field]; |
| 65 | if (got === undefined && field in optional) continue; | |
| 53 | 66 | if (typeof got !== kind || (kind === "number" && !Number.isFinite(got))) out.push(`${label}.${field} should be a ${kind}, is ${String(got)}`); |
| 54 | 67 | } |
| 55 | 68 | return out; |
| 56 | 69 | } |
| 57 | 70 | |
| 58 | 71 | /** What is wrong with one body of `snap.players`; empty when it is fit to draw. */ |
| 59 | export const bodyProblems = (body: unknown, label = "body"): string[] => problems(body, BODY, label); | |
| 72 | export const bodyProblems = (body: unknown, label = "body"): string[] => problems(body, BODY, label, BODY_OPTIONAL); | |
| 60 | 73 | |
| 61 | 74 | /** What is wrong with `snap.me`; empty when the gun and the camera can read it. */ |
| 62 | 75 | export const meProblems = (me: unknown): string[] => problems(me, ME, "me"); |
src/engine/boundary.test.ts +1
| @@ -59,6 +59,7 @@ test("the engine folders exist and hold the pieces a game stands on", () => { | ||
| 59 | 59 | "engine/budget.js", |
| 60 | 60 | "engine/topdown.js", |
| 61 | 61 | "engine/gun.js", |
| 62 | "engine/sides.js", | |
| 62 | 63 | ]) |
| 63 | 64 | expect(names).toContain(need); |
| 64 | 65 | }); |
src/engine/presence.test.ts +7
| @@ -160,3 +160,10 @@ test("two bodies are tracked apart", () => { | ||
| 160 | 160 | const marks = ghosts(p, 10.2); |
| 161 | 161 | expect(marks.map((m) => m.id)).toEqual(["b"]); |
| 162 | 162 | }); |
| 163 | ||
| 164 | test("a last-seen mark remembers the side the body was on", () => { | |
| 165 | const p = makePresence(); | |
| 166 | sighted(p, [body({ side: "third" })], 10); | |
| 167 | sighted(p, [], 10.2); | |
| 168 | expect(ghosts(p, 10.2)[0]!.side).toBe("third"); | |
| 169 | }); |
src/engine/scene.test.ts +64 −2
| @@ -11,7 +11,7 @@ import { expect, test } from "bun:test"; | ||
| 11 | 11 | import { GHOST, ghosts, makePresence, shown, sighted } from "../../static/engine/presence.js"; |
| 12 | 12 | import { drawScene, makeScene } from "../../static/engine/scene.js"; |
| 13 | 13 | |
| 14 | type Op = { op: string; args: unknown[]; alpha: number }; | |
| 14 | type Op = { op: string; args: unknown[]; alpha: number; fill: string; stroke: string }; | |
| 15 | 15 | |
| 16 | 16 | /** A canvas that records. Every call the renderer makes has to exist here. */ |
| 17 | 17 | function fakeContext() { |
| @@ -21,7 +21,7 @@ function fakeContext() { | ||
| 21 | 21 | (...args: unknown[]) => { |
| 22 | 22 | // The opacity in force when the call was made: how faded a body |
| 23 | 23 | // arriving into sight is, and how far a last-seen mark has dimmed. |
| 24 | ops.push({ op, args, alpha: ctx.globalAlpha }); | |
| 24 | ops.push({ op, args, alpha: ctx.globalAlpha, fill: ctx.fillStyle, stroke: ctx.strokeStyle }); | |
| 25 | 25 | }; |
| 26 | 26 | const ctx = { |
| 27 | 27 | ops, |
| @@ -234,3 +234,65 @@ test("a mark where the wall now stands between you and it is not drawn", () => { | ||
| 234 | 234 | drawScene(ctx as never, makeScene(), world, eye, snapshot([]), { ...view, marks }); |
| 235 | 235 | expect(ctx.ops.some((o) => o.op === "strokeRect")).toBe(false); |
| 236 | 236 | }); |
| 237 | ||
| 238 | // ------------------------------------------------------- colour, by side | |
| 239 | ||
| 240 | /** The fill of each upright body, left to right: the rect its edge is drawn round. */ | |
| 241 | function skins(ctx: ReturnType<typeof fakeContext>): string[] { | |
| 242 | const out: { x: number; fill: string }[] = []; | |
| 243 | ctx.ops.forEach((o, i) => { | |
| 244 | if (o.op !== "strokeRect") return; | |
| 245 | const body = ctx.ops[i - 1]!; | |
| 246 | if (body.op === "fillRect") out.push({ x: Number(body.args[0]), fill: body.fill }); | |
| 247 | }); | |
| 248 | return out.sort((a, b) => a.x - b.x).map((b) => b.fill); | |
| 249 | } | |
| 250 | ||
| 251 | test("without `side`, a mate and an enemy wear exactly today's colours", () => { | |
| 252 | const ctx = fakeContext(); | |
| 253 | drawScene(ctx as never, makeScene(), world, eye, snapshot([body({ id: "m", mate: true, y: 150 }), body({ id: "e", y: 250 })]), view); | |
| 254 | // Looking east, the smaller y is on the left. | |
| 255 | expect(skins(ctx)).toEqual(["#3f9fd6", "#d8483f"]); | |
| 256 | // And a last-seen enemy is outlined in today's colour too. | |
| 257 | const marked = fakeContext(); | |
| 258 | drawScene(marked as never, makeScene(), world, eye, snapshot([]), { | |
| 259 | ...view, | |
| 260 | marks: [{ id: "e", x: 280, y: 200, angle: 0, mate: false, fade: 1 }], | |
| 261 | }); | |
| 262 | expect(marked.ops.find((o) => o.op === "strokeRect")!.stroke).toBe("rgba(235,150,120,0.9)"); | |
| 263 | }); | |
| 264 | ||
| 265 | test("three enemy sides draw in three colours, and each keeps its colour next frame", () => { | |
| 266 | const three = [ | |
| 267 | body({ id: "a", side: "scene-north", y: 130 }), | |
| 268 | body({ id: "b", side: "scene-east", y: 200 }), | |
| 269 | body({ id: "c", side: "scene-west", y: 270 }), | |
| 270 | ]; | |
| 271 | const first = fakeContext(); | |
| 272 | drawScene(first as never, makeScene(), world, eye, snapshot(three), view); | |
| 273 | const colours = skins(first); | |
| 274 | expect(colours).toHaveLength(3); | |
| 275 | expect(new Set(colours).size).toBe(3); | |
| 276 | // None of them is the mate's blue. | |
| 277 | expect(colours).not.toContain("#3f9fd6"); | |
| 278 | ||
| 279 | // Next frame the server lists them the other way round: same colours. | |
| 280 | const second = fakeContext(); | |
| 281 | drawScene(second as never, makeScene(), world, eye, snapshot([...three].reverse()), view); | |
| 282 | expect(skins(second)).toEqual(colours); | |
| 283 | ||
| 284 | // And a side alone on screen still wears its own, not the first slot's. | |
| 285 | const alone = fakeContext(); | |
| 286 | drawScene(alone as never, makeScene(), world, eye, snapshot([three[2]]), view); | |
| 287 | expect(skins(alone)).toEqual([colours[2]!]); | |
| 288 | }); | |
| 289 | ||
| 290 | test("a mate with a side is still the mate's colour, and the dead are still grey", () => { | |
| 291 | const ctx = fakeContext(); | |
| 292 | drawScene(ctx as never, makeScene(), world, eye, snapshot([body({ mate: true, side: "scene-mine" })]), view); | |
| 293 | expect(skins(ctx)).toEqual(["#3f9fd6"]); | |
| 294 | ||
| 295 | const dead = fakeContext(); | |
| 296 | drawScene(dead as never, makeScene(), world, eye, snapshot([body({ side: "scene-dead", alive: false, hp: 0 })]), view); | |
| 297 | expect(dead.ops.find((o) => o.op === "fill")!.fill).toBe("#4a4e57"); | |
| 298 | }); |
src/engine/sides.test.ts added +34
| @@ -0,0 +1,34 @@ | ||
| 1 | /** Which colour a side is handed, and that it keeps it. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { ENEMY, enemyColours, makeSides, PALETTE } from "../../static/engine/sides.js"; | |
| 4 | ||
| 5 | test("a body with no side wears today's enemy colours, and takes no slot", () => { | |
| 6 | const book = makeSides(); | |
| 7 | expect(enemyColours({}, book)).toBe(ENEMY); | |
| 8 | expect(ENEMY).toEqual({ skin: "#d8483f", dot: "#e35d5b", mark: "rgba(235,150,120,0.9)" }); | |
| 9 | expect(book.size).toBe(0); | |
| 10 | }); | |
| 11 | ||
| 12 | test("sides are handed the palette in the order they are first seen, and keep it", () => { | |
| 13 | const book = makeSides(); | |
| 14 | const a = enemyColours({ side: "a" }, book); | |
| 15 | const b = enemyColours({ side: "b" }, book); | |
| 16 | const c = enemyColours({ side: "c" }, book); | |
| 17 | expect([a, b, c]).toEqual([PALETTE[0], PALETTE[1], PALETTE[2]]); | |
| 18 | // The first side is the enemy red a two-side game already wears. | |
| 19 | expect(a).toBe(ENEMY); | |
| 20 | expect(enemyColours({ side: "c" }, book)).toBe(c); | |
| 21 | expect(enemyColours({ side: "a" }, book)).toBe(a); | |
| 22 | }); | |
| 23 | ||
| 24 | test("the palette is all different, and none of it is a mate, the dead or you", () => { | |
| 25 | const all = PALETTE.flatMap((p) => [p.skin, p.dot]); | |
| 26 | expect(new Set(all).size).toBe(all.length); | |
| 27 | for (const taken of ["#3f9fd6", "#4fb3d9", "#eaeaea", "#555a63", "#4a4e57"]) expect(all).not.toContain(taken); | |
| 28 | }); | |
| 29 | ||
| 30 | test("more sides than colours wrap round rather than fail", () => { | |
| 31 | const book = makeSides(); | |
| 32 | for (let i = 0; i < PALETTE.length; i++) enemyColours({ side: `s${i}` }, book); | |
| 33 | expect(enemyColours({ side: "one more" }, book)).toBe(PALETTE[0]); | |
| 34 | }); |
src/engine/topdown.test.ts added +80
| @@ -0,0 +1,80 @@ | ||
| 1 | /** | |
| 2 | * The board from above, drawn into a canvas that writes down the colour of | |
| 3 | * every dot. What is pinned here is colour: who is drawn in which. | |
| 4 | */ | |
| 5 | import { expect, test } from "bun:test"; | |
| 6 | import { drawTopdown } from "../../static/engine/topdown.js"; | |
| 7 | ||
| 8 | function fakeContext() { | |
| 9 | const dots: { x: number; y: number; fill: string }[] = []; | |
| 10 | const rings: string[] = []; | |
| 11 | let at: { x: number; y: number } | null = null; | |
| 12 | const noop = () => {}; | |
| 13 | const ctx = { | |
| 14 | dots, | |
| 15 | rings, | |
| 16 | fillStyle: "", | |
| 17 | strokeStyle: "", | |
| 18 | lineWidth: 1, | |
| 19 | font: "", | |
| 20 | textAlign: "left", | |
| 21 | globalAlpha: 1, | |
| 22 | save: noop, | |
| 23 | restore: noop, | |
| 24 | beginPath: () => { | |
| 25 | at = null; | |
| 26 | }, | |
| 27 | closePath: noop, | |
| 28 | clip: noop, | |
| 29 | rect: noop, | |
| 30 | translate: noop, | |
| 31 | rotate: noop, | |
| 32 | moveTo: noop, | |
| 33 | lineTo: noop, | |
| 34 | fillRect: noop, | |
| 35 | fillText: noop, | |
| 36 | arc: (x: number, y: number) => { | |
| 37 | at = { x, y }; | |
| 38 | }, | |
| 39 | fill: () => { | |
| 40 | if (at) dots.push({ ...at, fill: ctx.fillStyle }); | |
| 41 | }, | |
| 42 | stroke: () => { | |
| 43 | if (ctx.globalAlpha < 1) rings.push(ctx.strokeStyle); | |
| 44 | }, | |
| 45 | }; | |
| 46 | return ctx; | |
| 47 | } | |
| 48 | ||
| 49 | const map = { rows: ["....", "....", "....", "...."], tile: 100 }; | |
| 50 | const box = { x: 0, y: 0, w: 400, h: 400 }; | |
| 51 | const me = { id: "me", name: "Me", x: 50, y: 50, angle: 0, alive: true, hp: 100, mate: true }; | |
| 52 | const body = (over: Record<string, unknown>) => ({ name: "B", y: 200, angle: 0, alive: true, hp: 100, mate: false, ...over }); | |
| 53 | const draw = (players: unknown[], marks?: unknown[]) => { | |
| 54 | const ctx = fakeContext(); | |
| 55 | drawTopdown(ctx as never, map, { me, players: [me, ...players], shots: [] }, (p: unknown) => p, box, { marks, labels: false }); | |
| 56 | return ctx; | |
| 57 | }; | |
| 58 | /** Body dots, left to right; the arrow that is you is not a dot. */ | |
| 59 | const dots = (ctx: ReturnType<typeof fakeContext>) => [...ctx.dots].sort((a, b) => a.x - b.x).map((d) => d.fill); | |
| 60 | ||
| 61 | test("without `side`, a mate, an enemy and a fallen mate wear exactly today's colours", () => { | |
| 62 | const ctx = draw([body({ id: "m", mate: true, x: 100 }), body({ id: "e", x: 200 }), body({ id: "d", mate: true, alive: false, x: 300 })]); | |
| 63 | expect(dots(ctx)).toEqual(["#4fb3d9", "#e35d5b"]); | |
| 64 | expect(draw([], [{ id: "e", x: 200, y: 200, angle: 0, mate: false, fade: 0.5 }]).rings).toEqual(["#e35d5b", "#e35d5b"]); | |
| 65 | }); | |
| 66 | ||
| 67 | test("three enemy sides are three colours on the map, the same frame after frame", () => { | |
| 68 | const three = [ | |
| 69 | body({ id: "a", side: "map-red", x: 100 }), | |
| 70 | body({ id: "b", side: "map-gold", x: 200 }), | |
| 71 | body({ id: "c", side: "map-violet", x: 300 }), | |
| 72 | ]; | |
| 73 | const first = dots(draw(three)); | |
| 74 | expect(new Set(first).size).toBe(3); | |
| 75 | expect(first).not.toContain("#4fb3d9"); | |
| 76 | expect(dots(draw([...three].reverse()))).toEqual(first); | |
| 77 | // A last-seen ring wears its side's colour, not the default enemy's. | |
| 78 | const ring = draw([], [{ id: "c", x: 300, y: 200, angle: 0, mate: false, side: "map-violet", fade: 0.5 }]).rings; | |
| 79 | expect(ring[0]).toBe(first[2]); | |
| 80 | }); |
static/engine/presence.js +2 −1
| @@ -74,6 +74,7 @@ function remember(it, body, seq) { | ||
| 74 | 74 | it.angle = body.angle; |
| 75 | 75 | it.alive = body.alive; |
| 76 | 76 | it.mate = body.mate; |
| 77 | it.side = body.side; | |
| 77 | 78 | } |
| 78 | 79 | |
| 79 | 80 | /** |
| @@ -124,7 +125,7 @@ export function ghosts(presence, now) { | ||
| 124 | 125 | presence.last.delete(id); |
| 125 | 126 | continue; |
| 126 | 127 | } |
| 127 | out.push({ id, x: it.x, y: it.y, angle: it.angle, mate: it.mate, fade: left }); | |
| 128 | out.push({ id, x: it.x, y: it.y, angle: it.angle, mate: it.mate, side: it.side, fade: left }); | |
| 128 | 129 | } |
| 129 | 130 | return out; |
| 130 | 131 | } |
static/engine/scene.js +3 −2
| @@ -20,6 +20,7 @@ | ||
| 20 | 20 | */ |
| 21 | 21 | |
| 22 | 22 | import { swell } from "./presence.js"; |
| 23 | import { enemyColours } from "./sides.js"; | |
| 23 | 24 | import { makeColumns, SKY, wallRuns } from "./slices.js"; |
| 24 | 25 | import { |
| 25 | 26 | castRay, |
| @@ -300,7 +301,7 @@ function drawBody(ctx, thing, floorY, clip, trim, badge) { | ||
| 300 | 301 | return; |
| 301 | 302 | } |
| 302 | 303 | |
| 303 | const skin = mate ? "#3f9fd6" : "#d8483f"; | |
| 304 | const skin = mate ? "#3f9fd6" : enemyColours(p).skin; | |
| 304 | 305 | const stripe = trim(p); |
| 305 | 306 | const mark = badge(p); |
| 306 | 307 | const edge = Math.max(1.2, h * 0.05); |
| @@ -350,7 +351,7 @@ function drawMark(ctx, thing, floorY, clip) { | ||
| 350 | 351 | const cx = thing.column; |
| 351 | 352 | const top = floorY - h; |
| 352 | 353 | clip(thing, cx - w * 1.2, cx + w * 1.2, () => { |
| 353 | ctx.strokeStyle = thing.mark.mate ? "rgba(120,180,230,0.85)" : "rgba(235,150,120,0.9)"; | |
| 354 | ctx.strokeStyle = thing.mark.mate ? "rgba(120,180,230,0.85)" : enemyColours(thing.mark).mark; | |
| 354 | 355 | ctx.lineWidth = Math.max(1, h * 0.035); |
| 355 | 356 | ctx.strokeRect(cx - w / 2, top + h * 0.28, w, h * 0.72); |
| 356 | 357 | ctx.fillStyle = "rgba(10,12,16,0.45)"; |
static/engine/sides.js added +55
| @@ -0,0 +1,55 @@ | ||
| 1 | /** | |
| 2 | * What colour a body is drawn in. | |
| 3 | * | |
| 4 | * Every body says `mate`: on the viewer's side or not. That is enough for a | |
| 5 | * game of two sides, and a game that sends nothing more looks exactly as it | |
| 6 | * always has. A game of three sides, or everybody for themselves, also sends | |
| 7 | * `side` — any string naming the body's side — and each side that is not the | |
| 8 | * viewer's gets its own colour from a small fixed palette. | |
| 9 | * | |
| 10 | * A side keeps the colour it was first given for as long as the page is | |
| 11 | * open, so it does not change hue between frames, or when another side walks | |
| 12 | * out of sight. The first side seen gets the enemy red a two-side game | |
| 13 | * already wears. More sides than the palette has wrap round to its start. | |
| 14 | * | |
| 15 | * Mates keep the mate colour, you keep yours and the dead keep their grey, | |
| 16 | * whatever side they are on: those are drawn by the callers, not here. | |
| 17 | */ | |
| 18 | ||
| 19 | /** Today's enemy, and what any body without a `side` wears. */ | |
| 20 | export const ENEMY = { skin: "#d8483f", dot: "#e35d5b", mark: "rgba(235,150,120,0.9)" }; | |
| 21 | ||
| 22 | /** | |
| 23 | * The palette, in the order sides are handed it. Each is `skin` in the first | |
| 24 | * person, `dot` on the map and `mark` for a last-seen outline. None is blue | |
| 25 | * (mates), grey (the dead) or white (you). | |
| 26 | */ | |
| 27 | export const PALETTE = [ | |
| 28 | ENEMY, | |
| 29 | { skin: "#e08a2c", dot: "#f0a040", mark: "rgba(240,175,100,0.9)" }, | |
| 30 | { skin: "#9b5fd6", dot: "#b07ae8", mark: "rgba(190,150,235,0.9)" }, | |
| 31 | { skin: "#4fae4a", dot: "#62c45c", mark: "rgba(140,210,130,0.9)" }, | |
| 32 | { skin: "#d655a8", dot: "#e86fbb", mark: "rgba(235,150,205,0.9)" }, | |
| 33 | { skin: "#c9b52e", dot: "#ddc944", mark: "rgba(225,210,120,0.9)" }, | |
| 34 | ]; | |
| 35 | ||
| 36 | /** A fresh book of which side has which colour. */ | |
| 37 | export const makeSides = () => new Map(); | |
| 38 | ||
| 39 | /** The one book the renderers share, so the map and the scene agree. */ | |
| 40 | const shared = makeSides(); | |
| 41 | ||
| 42 | /** | |
| 43 | * The colours for a body that is not on the viewer's side: by its `side` | |
| 44 | * when it carries one, today's enemy colours when it does not. | |
| 45 | */ | |
| 46 | export function enemyColours(body, book = shared) { | |
| 47 | const side = body.side; | |
| 48 | if (typeof side !== "string") return ENEMY; | |
| 49 | let at = book.get(side); | |
| 50 | if (at === undefined) { | |
| 51 | at = book.size % PALETTE.length; | |
| 52 | book.set(side, at); | |
| 53 | } | |
| 54 | return PALETTE[at]; | |
| 55 | } |
static/engine/topdown.js +4 −2
| @@ -11,6 +11,8 @@ | ||
| 11 | 11 | * for a badge, the game says through `opts` — see `drawTopdown`. |
| 12 | 12 | */ |
| 13 | 13 | |
| 14 | import { enemyColours } from "./sides.js"; | |
| 15 | ||
| 14 | 16 | const WALL_COLOUR = "#20242d"; |
| 15 | 17 | const FLOOR_COLOUR = "#171a21"; |
| 16 | 18 | |
| @@ -101,7 +103,7 @@ function drawMark(ctx, m, s) { | ||
| 101 | 103 | const r = Math.max(2.5, 16 * s); |
| 102 | 104 | ctx.save(); |
| 103 | 105 | ctx.globalAlpha = 0.8 * m.fade; |
| 104 | ctx.strokeStyle = m.mate ? "#4fb3d9" : "#e35d5b"; | |
| 106 | ctx.strokeStyle = m.mate ? "#4fb3d9" : enemyColours(m).dot; | |
| 105 | 107 | ctx.lineWidth = Math.max(1, 2 * s); |
| 106 | 108 | disc(ctx, x, y, r, false, true); |
| 107 | 109 | ctx.beginPath(); |
| @@ -130,7 +132,7 @@ function drawPlayer(ctx, p, pos, s, me, labels, trim, badges) { | ||
| 130 | 132 | const x = pos.x * s; |
| 131 | 133 | const y = pos.y * s; |
| 132 | 134 | const r = Math.max(2.5, 16 * s); |
| 133 | const body = !p.alive ? "#555a63" : p.mate ? (mine ? "#eaeaea" : "#4fb3d9") : "#e35d5b"; | |
| 135 | const body = !p.alive ? "#555a63" : p.mate ? (mine ? "#eaeaea" : "#4fb3d9") : enemyColours(p).dot; | |
| 134 | 136 | |
| 135 | 137 | if (mine && p.alive) { |
| 136 | 138 | ctx.save(); |