Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

dumduel: top-down arena, 2D movement, slower bullets, practice dummy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+161 -189
+161 -189
system/public/aesthetic.computer/disks/dumduel.mjs
··· 1 1 // Dumduel, 2026.03.30 2 - // Stick figure shootout. Tap to run, dodge bullets, instant death. 2 + // Top-down stick figure shootout. Tap to run, dodge bullets, instant death. 3 3 // Two duelists at a time — everyone else waits in the stack. 4 4 5 - const ARENA_W = 200; 6 - const ARENA_H = 140; 7 - const GROUND_Y = ARENA_H - 16; 8 - const FIGURE_H = 16; 9 - const FIGURE_W = 6; 10 - const BULLET_SPEED = 2.5; 5 + const ARENA_W = 180; 6 + const ARENA_H = 180; 7 + const BULLET_SPEED = 1.2; 11 8 const BULLET_R = 2; 12 - const MOVE_SPEED = 1.2; 13 - const FIRE_INTERVAL = 90; // frames between auto-shots (~1.5s) 9 + const MOVE_SPEED = 1.0; 10 + const FIRE_INTERVAL = 100; // frames between auto-shots (~1.7s) 14 11 const COUNTDOWN_FRAMES = 180; // 3 seconds 15 12 const ROUND_OVER_FRAMES = 120; // 2 seconds pause after kill 16 - const HIT_R = 5; // hitbox radius 13 + const HIT_R = 5; 14 + const BODY_R = 4; // figure body radius (top-down circle) 17 15 18 16 // -- State -- 19 17 let server, udpChannel; ··· 22 20 let sw = 0, sh = 0; 23 21 let frameCount = 0; 24 22 25 - // All connected players in order: [{ id, handle }] 26 - // First two are the duelists, rest are the stack 27 - let roster = []; 28 - 29 - // Game phase: "waiting" | "countdown" | "fight" | "roundover" 30 - let phase = "waiting"; 23 + let roster = []; // [{ id, handle }] — first two duel, rest wait 24 + let phase = "waiting"; // waiting | countdown | fight | roundover 31 25 let countdownTimer = 0; 32 26 let roundOverTimer = 0; 33 - let roundWinner = null; // handle of winner 27 + let roundWinner = null; 34 28 35 - // Local duelist state (only active when I'm dueling) 36 - let me = null; // { x, y, targetX, alive, fireTimer } 37 - let opponent = null; // { x, y, targetX, alive, fireTimer, handle } 38 - let bullets = []; // { x, y, vx, owner: "me"|"them" } 39 - let mySlot = -1; // 0 = left, 1 = right 40 - let dummy = false; // true when practicing solo against a dummy 29 + let me = null; // { x, y, targetX, targetY, alive, fireTimer } 30 + let opponent = null; // { x, y, targetX, targetY, alive, fireTimer, handle } 31 + let bullets = []; // { x, y, vx, vy, owner: "me"|"them" } 32 + let mySlot = -1; 33 + let dummy = false; 41 34 42 35 function isDueling() { 43 36 return roster.length >= 2 && ··· 50 43 51 44 function spawnDuelists() { 52 45 bullets = []; 46 + // Spawn in opposite corners 47 + const pos0 = { x: 30, y: 30 }; 48 + const pos1 = { x: ARENA_W - 30, y: ARENA_H - 30 }; 49 + const myPos = mySlot === 0 ? pos0 : pos1; 50 + const opPos = mySlot === 0 ? pos1 : pos0; 51 + 53 52 me = { 54 - x: mySlot === 0 ? 30 : ARENA_W - 30, 55 - y: GROUND_Y, 56 - targetX: mySlot === 0 ? 30 : ARENA_W - 30, 57 - alive: true, 58 - fireTimer: FIRE_INTERVAL, 53 + x: myPos.x, y: myPos.y, 54 + targetX: myPos.x, targetY: myPos.y, 55 + alive: true, fireTimer: FIRE_INTERVAL, 59 56 }; 60 57 opponent = { 61 - x: mySlot === 0 ? ARENA_W - 30 : 30, 62 - y: GROUND_Y, 63 - targetX: mySlot === 0 ? ARENA_W - 30 : 30, 64 - alive: true, 65 - fireTimer: FIRE_INTERVAL, 58 + x: opPos.x, y: opPos.y, 59 + targetX: opPos.x, targetY: opPos.y, 60 + alive: true, fireTimer: FIRE_INTERVAL, 66 61 handle: roster[mySlot === 0 ? 1 : 0]?.handle || "???", 67 62 }; 68 63 } 69 64 70 65 function startPractice() { 71 66 dummy = true; 72 - // Add dummy to roster as slot 1 73 67 if (!roster.find((r) => r.handle === "dummy")) { 74 68 roster.push({ id: "dummy", handle: "dummy" }); 75 69 } ··· 83 77 function stopPractice() { 84 78 dummy = false; 85 79 roster = roster.filter((r) => r.handle !== "dummy"); 86 - me = null; 87 - opponent = null; 88 - bullets = []; 80 + me = null; opponent = null; bullets = []; 89 81 } 90 82 91 83 function startCountdown() { 92 84 phase = "countdown"; 93 85 countdownTimer = COUNTDOWN_FRAMES; 94 - 95 - // Determine slots 96 86 const idx = myRosterIdx(); 97 87 if (idx === 0) mySlot = 0; 98 88 else if (idx === 1) mySlot = 1; 99 - else mySlot = -1; // spectating 100 - 89 + else mySlot = -1; 101 90 if (isDueling()) spawnDuelists(); 102 91 } 103 92 ··· 113 102 } 114 103 115 104 function advanceStack() { 116 - // Loser goes to bottom of stack 117 105 if (roster.length >= 2) { 118 106 const loserIdx = roster[0].handle === roundWinner ? 1 : 0; 119 107 const loser = roster.splice(loserIdx, 1)[0]; 120 108 roster.push(loser); 121 109 } 122 110 roundWinner = null; 123 - me = null; 124 - opponent = null; 125 - bullets = []; 111 + me = null; opponent = null; bullets = []; 112 + if (roster.length >= 2) startCountdown(); 113 + else startPractice(); 114 + } 126 115 127 - if (roster.length >= 2) { 128 - startCountdown(); 129 - } else { 130 - // Back to solo — restart practice 131 - startPractice(); 132 - } 116 + // Normalize a 2D vector 117 + function norm(dx, dy) { 118 + const len = Math.sqrt(dx * dx + dy * dy); 119 + if (len < 0.001) return { nx: 0, ny: 0 }; 120 + return { nx: dx / len, ny: dy / len }; 133 121 } 134 122 135 123 function boot({ wipe, screen, net: { socket, udp }, handle }) { ··· 140 128 udpChannel = udp((type, content) => { 141 129 const d = typeof content === "string" ? JSON.parse(content) : content; 142 130 if (d.handle === myHandle) return; 143 - 144 131 if (type === "duel:pos" && opponent) { 145 - opponent.x = d.x; 146 - opponent.y = d.y; 147 - opponent.targetX = d.targetX; 132 + opponent.x = d.x; opponent.y = d.y; 133 + opponent.targetX = d.tx; opponent.targetY = d.ty; 148 134 } 149 135 }); 150 136 ··· 161 147 const wasDueling = idx < 2 && roster.length >= 2; 162 148 roster.splice(idx, 1); 163 149 if (wasDueling && (phase === "fight" || phase === "countdown")) { 164 - // Opponent left mid-duel — current player wins 165 - if (isDueling()) { 166 - endRound(myHandle); 167 - } else { 168 - phase = "waiting"; 169 - me = null; opponent = null; bullets = []; 170 - } 150 + if (isDueling()) endRound(myHandle); 151 + else { phase = "waiting"; me = null; opponent = null; bullets = []; } 171 152 } 172 153 } 173 154 return; ··· 176 157 const msg = typeof content === "string" ? JSON.parse(content) : content; 177 158 178 159 if (type === "duel:join" && msg.handle !== myHandle) { 179 - // Stop practice if a real player joins 180 160 if (dummy) stopPractice(); 181 161 if (!roster.find((r) => r.handle === msg.handle)) { 182 162 roster.push({ id, handle: msg.handle }); 183 163 } 184 - // Send full roster to newcomer 185 164 server.send("duel:roster", { 186 165 handle: myHandle, 187 166 roster: roster.map((r) => ({ handle: r.handle })), 188 167 phase, 189 168 }); 190 - // If we now have 2+ and were waiting, start 191 169 if (roster.length >= 2 && phase === "waiting") { 192 170 startCountdown(); 193 171 server.send("duel:countdown", { handle: myHandle }); ··· 195 173 } 196 174 197 175 if (type === "duel:roster") { 198 - // Sync roster from existing player 199 176 if (roster.length <= 1) { 200 177 for (const r of msg.roster) { 201 178 if (!roster.find((x) => x.handle === r.handle)) { 202 179 roster.push({ id: r.handle === msg.handle ? id : "?", handle: r.handle }); 203 180 } 204 181 } 205 - if (msg.phase === "countdown" || msg.phase === "fight") { 206 - startCountdown(); 207 - } 182 + if (msg.phase === "countdown" || msg.phase === "fight") startCountdown(); 208 183 } 209 184 } 210 185 211 186 if (type === "duel:countdown") { 212 - if (roster.length >= 2 && phase === "waiting") { 213 - startCountdown(); 214 - } 187 + if (roster.length >= 2 && phase === "waiting") startCountdown(); 215 188 } 216 189 217 - if (type === "duel:fire") { 218 - // Opponent fired a bullet 219 - if (phase === "fight") { 220 - bullets.push({ 221 - x: msg.x, y: msg.y, vx: msg.vx, owner: "them", 222 - }); 223 - } 190 + if (type === "duel:fire" && phase === "fight") { 191 + bullets.push({ x: msg.x, y: msg.y, vx: msg.vx, vy: msg.vy, owner: "them" }); 224 192 } 225 193 226 - if (type === "duel:hit") { 227 - // Someone got hit — trust the shooter's claim 228 - if (phase === "fight") { 229 - if (msg.victim === myHandle) { 230 - if (me) me.alive = false; 231 - endRound(msg.handle); 232 - server.send("duel:roundover", { winner: msg.handle }); 233 - } 194 + if (type === "duel:hit" && phase === "fight") { 195 + if (msg.victim === myHandle) { 196 + if (me) me.alive = false; 197 + endRound(msg.handle); 198 + server.send("duel:roundover", { winner: msg.handle }); 234 199 } 235 200 } 236 201 237 - if (type === "duel:roundover") { 238 - if (phase === "fight") { 239 - endRound(msg.winner); 240 - if (opponent && msg.winner === myHandle) { 241 - opponent.alive = false; 242 - } else if (me && msg.winner !== myHandle) { 243 - me.alive = false; 244 - } 245 - } 202 + if (type === "duel:roundover" && phase === "fight") { 203 + endRound(msg.winner); 204 + if (opponent && msg.winner === myHandle) opponent.alive = false; 205 + else if (me && msg.winner !== myHandle) me.alive = false; 246 206 } 247 207 248 - if (type === "duel:advance") { 249 - advanceStack(); 250 - } 208 + if (type === "duel:advance") advanceStack(); 251 209 }); 252 210 253 - // Add self to roster and start practice 254 211 roster.push({ id: myId, handle: myHandle }); 255 212 startPractice(); 256 - 257 213 wipe(240, 238, 232); 258 214 } 259 215 ··· 275 231 276 232 if (phase !== "fight" || !me || (!isDueling() && !dummy)) return; 277 233 278 - // Dummy AI — wander randomly 234 + // Dummy AI — wander randomly in 2D 279 235 if (dummy && opponent?.alive) { 280 236 opponent.fireTimer--; 281 - // Pick a new random target every ~2s 282 - if (frameCount % 120 === 0) { 237 + if (frameCount % 90 === 0) { 283 238 opponent.targetX = 20 + Math.random() * (ARENA_W - 40); 239 + opponent.targetY = 20 + Math.random() * (ARENA_H - 40); 284 240 } 285 241 const odx = opponent.targetX - opponent.x; 286 - if (Math.abs(odx) > 1) opponent.x += Math.sign(odx) * MOVE_SPEED * 0.8; 287 - // Dummy fires 242 + const ody = opponent.targetY - opponent.y; 243 + const dist = Math.sqrt(odx * odx + ody * ody); 244 + if (dist > 1) { 245 + opponent.x += (odx / dist) * MOVE_SPEED * 0.7; 246 + opponent.y += (ody / dist) * MOVE_SPEED * 0.7; 247 + } 288 248 if (opponent.fireTimer <= 0) { 289 249 opponent.fireTimer = FIRE_INTERVAL; 290 - const dir = Math.sign(me.x - opponent.x); 250 + const { nx, ny } = norm(me.x - opponent.x, me.y - opponent.y); 291 251 bullets.push({ 292 - x: opponent.x + dir * 4, 293 - y: opponent.y - FIGURE_H / 2, 294 - vx: dir * BULLET_SPEED, 252 + x: opponent.x + nx * 6, y: opponent.y + ny * 6, 253 + vx: nx * BULLET_SPEED, vy: ny * BULLET_SPEED, 295 254 owner: "them", 296 255 }); 297 256 } 298 257 } 299 258 300 - // Move toward target 259 + // Move me toward target 301 260 if (me.alive) { 302 261 const dx = me.targetX - me.x; 303 - if (Math.abs(dx) > 1) me.x += Math.sign(dx) * MOVE_SPEED; 304 - else me.x = me.targetX; 262 + const dy = me.targetY - me.y; 263 + const dist = Math.sqrt(dx * dx + dy * dy); 264 + if (dist > 1) { 265 + me.x += (dx / dist) * MOVE_SPEED; 266 + me.y += (dy / dist) * MOVE_SPEED; 267 + } 305 268 } 306 269 307 - // Opponent movement (interpolated from UDP) 308 - if (opponent) { 270 + // Interpolate opponent (non-dummy) 271 + if (!dummy && opponent) { 309 272 const dx = opponent.targetX - opponent.x; 310 - if (Math.abs(dx) > 1) opponent.x += Math.sign(dx) * MOVE_SPEED; 273 + const dy = opponent.targetY - opponent.y; 274 + const dist = Math.sqrt(dx * dx + dy * dy); 275 + if (dist > 1) { 276 + opponent.x += (dx / dist) * MOVE_SPEED; 277 + opponent.y += (dy / dist) * MOVE_SPEED; 278 + } 311 279 } 312 280 313 - // Auto-fire 281 + // Auto-fire toward opponent 314 282 if (me.alive) { 315 283 me.fireTimer--; 316 - if (me.fireTimer <= 0) { 284 + if (me.fireTimer <= 0 && opponent) { 317 285 me.fireTimer = FIRE_INTERVAL; 318 - const dir = opponent ? Math.sign(opponent.x - me.x) : (mySlot === 0 ? 1 : -1); 319 - const bx = me.x + dir * 4; 320 - const by = me.y - FIGURE_H / 2; 321 - bullets.push({ x: bx, y: by, vx: dir * BULLET_SPEED, owner: "me" }); 286 + const { nx, ny } = norm(opponent.x - me.x, opponent.y - me.y); 287 + const bx = me.x + nx * 6; 288 + const by = me.y + ny * 6; 289 + bullets.push({ x: bx, y: by, vx: nx * BULLET_SPEED, vy: ny * BULLET_SPEED, owner: "me" }); 322 290 server?.send("duel:fire", { 323 - handle: myHandle, x: bx, y: by, vx: dir * BULLET_SPEED, 291 + handle: myHandle, x: bx, y: by, vx: nx * BULLET_SPEED, vy: ny * BULLET_SPEED, 324 292 }); 325 293 } 326 294 } ··· 329 297 for (let i = bullets.length - 1; i >= 0; i--) { 330 298 const b = bullets[i]; 331 299 b.x += b.vx; 300 + b.y += b.vy; 332 301 333 - // Off screen 334 - if (b.x < -10 || b.x > ARENA_W + 10) { 302 + // Off arena 303 + if (b.x < -10 || b.x > ARENA_W + 10 || b.y < -10 || b.y > ARENA_H + 10) { 335 304 bullets.splice(i, 1); 336 305 continue; 337 306 } 338 307 339 308 // Hit detection 340 309 if (b.owner === "them" && me.alive) { 341 - const dx = b.x - me.x; 342 - const dy = b.y - (me.y - FIGURE_H / 2); 310 + const dx = b.x - me.x, dy = b.y - me.y; 343 311 if (dx * dx + dy * dy < HIT_R * HIT_R) { 344 312 me.alive = false; 345 313 bullets.splice(i, 1); ··· 349 317 } 350 318 } 351 319 if (b.owner === "me" && opponent?.alive) { 352 - const dx = b.x - opponent.x; 353 - const dy = b.y - (opponent.y - FIGURE_H / 2); 320 + const dx = b.x - opponent.x, dy = b.y - opponent.y; 354 321 if (dx * dx + dy * dy < HIT_R * HIT_R) { 355 322 opponent.alive = false; 356 323 bullets.splice(i, 1); 357 324 endRound(myHandle); 358 - server?.send("duel:hit", { 359 - handle: myHandle, victim: opponent.handle, 360 - }); 325 + server?.send("duel:hit", { handle: myHandle, victim: opponent.handle }); 361 326 server?.send("duel:roundover", { winner: myHandle }); 362 327 break; 363 328 } ··· 367 332 // Send position via UDP every 3 frames 368 333 if (frameCount % 3 === 0 && udpChannel?.connected && me.alive) { 369 334 udpChannel.send("duel:pos", { 370 - handle: myHandle, 371 - x: me.x, 372 - y: me.y, 373 - targetX: me.targetX, 335 + handle: myHandle, x: me.x, y: me.y, tx: me.targetX, ty: me.targetY, 374 336 }); 375 337 } 376 338 } ··· 380 342 sh = screen.height; 381 343 382 344 if (e.is("touch") && phase === "fight" && (isDueling() || dummy) && me?.alive) { 383 - // Tap to set run target (screen -> arena coords) 384 345 const ox = Math.floor(sw / 2 - ARENA_W / 2); 385 - const tx = e.x - ox; 386 - me.targetX = Math.max(8, Math.min(ARENA_W - 8, tx)); 346 + const oy = Math.floor(sh / 2 - ARENA_H / 2); 347 + me.targetX = Math.max(6, Math.min(ARENA_W - 6, e.x - ox)); 348 + me.targetY = Math.max(6, Math.min(ARENA_H - 6, e.y - oy)); 387 349 } 388 350 } 389 351 390 - function paint({ wipe, ink, box, write, line, circle, screen }) { 352 + function paint({ wipe, ink, box, write, circle, screen }) { 391 353 sw = screen.width; 392 354 sh = screen.height; 393 355 wipe(240, 238, 232); ··· 395 357 const ox = Math.floor(sw / 2 - ARENA_W / 2); 396 358 const oy = Math.floor(sh / 2 - ARENA_H / 2); 397 359 398 - // Arena bg 399 - ink(255, 253, 248).box(ox, oy, ARENA_W, ARENA_H); 400 - // Ground 401 - ink(220, 215, 205).box(ox, oy + GROUND_Y, ARENA_W, ARENA_H - GROUND_Y); 402 - // Arena border 360 + // Arena 361 + ink(252, 250, 245).box(ox, oy, ARENA_W, ARENA_H); 403 362 ink(180, 175, 165).box(ox, oy, ARENA_W, ARENA_H, "outline"); 404 363 364 + // Center cross mark 365 + ink(230, 225, 218).box(ox + ARENA_W / 2 - 1, oy + ARENA_H / 2 - 6, 2, 12); 366 + ink(230, 225, 218).box(ox + ARENA_W / 2 - 6, oy + ARENA_H / 2 - 1, 12, 2); 367 + 405 368 if (phase === "countdown") { 406 369 const secs = Math.ceil(countdownTimer / 60); 407 - const numStr = "" + secs; 408 - ink(60, 55, 45).write(numStr, { 409 - x: ox + Math.floor(ARENA_W / 2 - numStr.length * 3), 410 - y: oy + 30, 370 + ink(60, 55, 45).write("" + secs, { 371 + x: ox + Math.floor(ARENA_W / 2 - 3), 372 + y: oy + Math.floor(ARENA_H / 2 - 3), 411 373 }); 412 374 413 375 if (me && opponent) { 414 - drawStickFigure(ink, line, circle, box, ox, oy, me, [50, 120, 200]); 415 - drawStickFigure(ink, line, circle, box, ox, oy, opponent, [200, 70, 60]); 376 + drawFigure(ink, circle, box, ox, oy, me, [50, 120, 200]); 377 + drawFigure(ink, circle, box, ox, oy, opponent, [200, 70, 60]); 416 378 } 417 379 418 380 const d0 = roster[0]?.handle || "?"; 419 381 const d1 = roster[1]?.handle || "?"; 420 - const vsStr = d0 + " vs " + d1; 421 - ink(140, 135, 125).write(vsStr, { 422 - x: ox + Math.floor(ARENA_W / 2 - vsStr.length * 3), 423 - y: oy + 50, 382 + const vs = d0 + " vs " + d1; 383 + ink(160, 155, 145).write(vs, { 384 + x: ox + Math.floor(ARENA_W / 2 - vs.length * 3), 385 + y: oy + ARENA_H + 4, 424 386 }); 425 387 } 426 388 ··· 432 394 circle(ox + Math.floor(b.x), oy + Math.floor(b.y), BULLET_R, true); 433 395 } 434 396 397 + // Target indicator (my tap destination) 398 + if (me?.alive && phase === "fight") { 399 + ink(50, 120, 200, 60).circle( 400 + ox + Math.floor(me.targetX), 401 + oy + Math.floor(me.targetY), 402 + 3, false, 403 + ); 404 + } 405 + 435 406 if (me && opponent) { 436 - drawStickFigure(ink, line, circle, box, ox, oy, me, [50, 120, 200]); 437 - drawStickFigure(ink, line, circle, box, ox, oy, opponent, [200, 70, 60]); 407 + drawFigure(ink, circle, box, ox, oy, me, [50, 120, 200]); 408 + drawFigure(ink, circle, box, ox, oy, opponent, [200, 70, 60]); 409 + } 410 + 411 + // Handle labels 412 + if (me) { 413 + ink(50, 120, 200, 150).write(myHandle, { 414 + x: ox + Math.floor(me.x) - myHandle.length * 3, 415 + y: oy + Math.floor(me.y) + BODY_R + 3, 416 + }); 417 + } 418 + if (opponent) { 419 + ink(200, 70, 60, 150).write(opponent.handle, { 420 + x: ox + Math.floor(opponent.x) - opponent.handle.length * 3, 421 + y: oy + Math.floor(opponent.y) + BODY_R + 3, 422 + }); 438 423 } 439 424 440 425 if (phase === "roundover" && roundWinner) { ··· 443 428 if (won) ink(50, 160, 80); else ink(200, 70, 60); 444 429 write(msg, { 445 430 x: ox + Math.floor(ARENA_W / 2 - msg.length * 3), 446 - y: oy + 20, 447 - }); 448 - } 449 - 450 - if (roster.length >= 2) { 451 - const d0 = roster[0]?.handle || "?"; 452 - const d1 = roster[1]?.handle || "?"; 453 - ink(140, 135, 125).write(d0, { x: ox + 2, y: oy + ARENA_H + 4 }); 454 - ink(140, 135, 125).write(d1, { 455 - x: ox + ARENA_W - d1.length * 6 - 2, y: oy + ARENA_H + 4, 431 + y: oy - 12, 456 432 }); 457 433 } 458 434 } 459 435 460 436 // Practice label 461 437 if (dummy && (phase === "fight" || phase === "countdown")) { 462 - ink(190, 185, 175).write("practice", { 438 + ink(200, 195, 185).write("practice", { 463 439 x: ox + Math.floor(ARENA_W / 2 - 24), 464 - y: oy - 10, 440 + y: oy - 12, 465 441 }); 466 442 } 467 443 468 - // -- Stack (queue) display -- 444 + // Stack 469 445 const stackX = ox + ARENA_W + 8; 470 446 const stackY = oy; 471 447 ink(160, 155, 145).write("stack", { x: stackX, y: stackY }); 448 + let si = 0; 472 449 for (let i = 0; i < roster.length; i++) { 473 450 const r = roster[i]; 474 - if (r.handle === "dummy") continue; // don't show dummy in stack 475 - const isMe = r.handle === myHandle; 451 + if (r.handle === "dummy") continue; 476 452 const isDuelist = i < 2 && roster.length >= 2; 477 453 if (isDuelist) ink(60, 55, 45); 478 - else if (isMe) ink(120, 115, 105); 454 + else if (r.handle === myHandle) ink(120, 115, 105); 479 455 else ink(170, 165, 155); 480 - write(r.handle, { x: stackX, y: stackY + 12 + i * 10 }); 456 + write(r.handle, { x: stackX, y: stackY + 12 + si * 10 }); 457 + si++; 481 458 } 482 459 } 483 460 484 - function drawStickFigure(ink, line, circle, box, ox, oy, fig, col) { 461 + function drawFigure(ink, circle, box, ox, oy, fig, col) { 485 462 const fx = ox + Math.floor(fig.x); 486 463 const fy = oy + Math.floor(fig.y); 487 464 488 465 if (!fig.alive) { 489 - // Dead — fallen over 490 - ink(col[0], col[1], col[2], 80); 491 - line(fx - 6, fy - 1, fx + 6, fy - 1); 492 - circle(fx + 7, fy - 2, 2, true); 466 + ink(col[0], col[1], col[2], 60); 467 + circle(fx, fy, BODY_R, false); 468 + ink(col[0], col[1], col[2], 40); 469 + box(fx - 1, fy - 1, 2, 2); 493 470 return; 494 471 } 495 472 473 + // Body (filled circle) 496 474 ink(...col); 497 - // Head 498 - circle(fx, fy - FIGURE_H, 3, true); 499 - // Body 500 - line(fx, fy - FIGURE_H + 3, fx, fy - 4); 501 - // Arms 502 - line(fx - 4, fy - FIGURE_H + 7, fx + 4, fy - FIGURE_H + 7); 503 - // Legs 504 - line(fx, fy - 4, fx - 3, fy); 505 - line(fx, fy - 4, fx + 3, fy); 475 + circle(fx, fy, BODY_R, true); 476 + // Inner highlight 477 + ink(col[0] + 40, col[1] + 40, col[2] + 40).circle(fx, fy, BODY_R - 2, false); 506 478 } 507 479 508 480 function meta() { 509 481 return { 510 482 title: "Dumduel", 511 - desc: "Stick figure shootout. Tap to dodge. Instant death. Winner stays.", 483 + desc: "Top-down stick figure shootout. Tap to dodge. Instant death.", 512 484 }; 513 485 } 514 486