Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

dumduel: animated legs, centered handles, bigger arena, fire on stop, auto-reload

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

+50 -64
+50 -64
system/public/aesthetic.computer/disks/dumduel.mjs
··· 2 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 = 180; 6 - const ARENA_H = 180; 5 + const ARENA_W = 220; 6 + const ARENA_H = 220; 7 7 const BULLET_SPEED = 0.7; 8 8 const BULLET_R = 2; 9 9 const MOVE_SPEED = 1.0; 10 - const FIRE_INTERVAL = 100; // frames between auto-shots (~1.7s) 11 - const COUNTDOWN_FRAMES = 180; // 3 seconds 12 - const ROUND_OVER_FRAMES = 120; // 2 seconds pause after kill 13 - const HIT_R = 5; 14 - const BODY_R = 4; // figure body radius (top-down circle) 10 + const COUNTDOWN_FRAMES = 180; 11 + const ROUND_OVER_FRAMES = 120; 12 + const HIT_R = 7; 13 + const BODY_R = 4; 15 14 16 15 // -- State -- 17 16 let server, udpChannel; ··· 29 28 let roundOverTimer = 0; 30 29 let roundWinner = null; 31 30 32 - let me = null; // { x, y, targetX, targetY, alive, fireTimer } 33 - let opponent = null; // { x, y, targetX, targetY, alive, fireTimer, handle } 31 + let me = null; // { x, y, targetX, targetY, alive, wasMoving } 32 + let opponent = null; // { x, y, targetX, targetY, alive, handle } 34 33 let bullets = []; // { x, y, vx, vy, owner: "me"|"them" } 35 34 let mySlot = -1; 36 35 let dummy = false; ··· 55 54 me = { 56 55 x: myPos.x, y: myPos.y, 57 56 targetX: myPos.x, targetY: myPos.y, 58 - alive: true, fireTimer: FIRE_INTERVAL, 57 + alive: true, wasMoving: false, 59 58 }; 60 59 opponent = { 61 60 x: opPos.x, y: opPos.y, 62 61 targetX: opPos.x, targetY: opPos.y, 63 - alive: true, fireTimer: FIRE_INTERVAL, 62 + alive: true, 64 63 handle: roster[mySlot === 0 ? 1 : 0]?.handle || "???", 65 64 }; 66 65 } ··· 153 152 if (!r.ok) break; 154 153 const data = await r.json(); 155 154 if (data.changed !== false) { 156 - updateAvailable = true; 155 + // Auto-reload on new deploy 156 + sendFn?.({ type: "window:reload" }); 157 157 break; 158 158 } 159 159 } catch { break; } ··· 291 291 const dx = me.targetX - me.x; 292 292 const dy = me.targetY - me.y; 293 293 const dist = Math.sqrt(dx * dx + dy * dy); 294 - if (dist > 1) { 294 + const isMoving = dist > 2; 295 + if (isMoving) { 295 296 me.x += (dx / dist) * MOVE_SPEED; 296 297 me.y += (dy / dist) * MOVE_SPEED; 297 298 } 298 - } 299 299 300 - // Interpolate opponent (non-dummy) 301 - if (!dummy && opponent) { 302 - const dx = opponent.targetX - opponent.x; 303 - const dy = opponent.targetY - opponent.y; 304 - const dist = Math.sqrt(dx * dx + dy * dy); 305 - if (dist > 1) { 306 - opponent.x += (dx / dist) * MOVE_SPEED; 307 - opponent.y += (dy / dist) * MOVE_SPEED; 308 - } 309 - } 310 - 311 - // Auto-fire toward opponent (one bullet at a time) 312 - if (me.alive) { 313 - me.fireTimer--; 300 + // Fire on stop (was moving, now stopped, no bullet in flight) 314 301 const myBulletOut = bullets.some((b) => b.owner === "me"); 315 - const meMoving = Math.abs(me.targetX - me.x) > 2 || Math.abs(me.targetY - me.y) > 2; 316 - if (me.fireTimer <= 0 && opponent && !myBulletOut && !meMoving) { 317 - me.fireTimer = FIRE_INTERVAL; 302 + if (me.wasMoving && !isMoving && !myBulletOut && opponent) { 318 303 const { nx, ny } = norm(opponent.x - me.x, opponent.y - me.y); 319 304 const bx = me.x + nx * 6; 320 305 const by = me.y + ny * 6; ··· 324 309 handle: myHandle, x: bx, y: by, vx: nx * BULLET_SPEED, vy: ny * BULLET_SPEED, 325 310 }); 326 311 } 312 + me.wasMoving = isMoving; 313 + } 314 + 315 + // Interpolate opponent (non-dummy) 316 + if (!dummy && opponent) { 317 + const dx = opponent.targetX - opponent.x; 318 + const dy = opponent.targetY - opponent.y; 319 + const dist = Math.sqrt(dx * dx + dy * dy); 320 + if (dist > 1) { 321 + opponent.x += (dx / dist) * MOVE_SPEED; 322 + opponent.y += (dy / dist) * MOVE_SPEED; 323 + } 327 324 } 328 325 329 326 // Update bullets ··· 384 381 if (e.is("touch")) { 385 382 const ox = Math.floor(sw / 2 - ARENA_W / 2); 386 383 const oy = Math.floor(sh / 2 - ARENA_H / 2); 387 - 388 - // Tap update banner to reload 389 - if (updateAvailable && e.y >= oy + ARENA_H + 14 && e.y < oy + ARENA_H + 38 && e.x >= ox && e.x < ox + ARENA_W) { 390 - sendFn?.({ type: "window:reload" }); 391 - return; 392 - } 393 384 394 385 // Tap arena to move 395 386 if (phase === "fight" && (isDueling() || dummy) && me?.alive) { ··· 423 414 }); 424 415 425 416 if (me && opponent) { 426 - drawFigure(ink, circle, box, line, ox, oy, me, [50, 120, 200]); 427 - drawFigure(ink, circle, box, line, ox, oy, opponent, [200, 70, 60]); 417 + drawFigure(ink, circle, box, line, ox, oy, me, [50, 120, 200], frameCount); 418 + drawFigure(ink, circle, box, line, ox, oy, opponent, [200, 70, 60], frameCount); 428 419 } 429 420 430 421 const d0 = roster[0]?.handle || "?"; ··· 455 446 } 456 447 457 448 if (me && opponent) { 458 - drawFigure(ink, circle, box, line, ox, oy, me, [50, 120, 200]); 459 - drawFigure(ink, circle, box, line, ox, oy, opponent, [200, 70, 60]); 449 + drawFigure(ink, circle, box, line, ox, oy, me, [50, 120, 200], frameCount); 450 + drawFigure(ink, circle, box, line, ox, oy, opponent, [200, 70, 60], frameCount); 460 451 } 461 452 462 - // Handle labels (MatrixChunky8) 453 + // Handle labels (MatrixChunky8, centered) 463 454 if (me) { 464 455 ink(50, 120, 200, 150).write(myHandle, { 465 - x: ox + Math.floor(me.x) - myHandle.length * 3, 466 - y: oy + Math.floor(me.y) + BODY_R + 5, 456 + x: ox + Math.floor(me.x) - Math.floor(myHandle.length * 2), 457 + y: oy + Math.floor(me.y) + 9, 467 458 }, undefined, undefined, false, "MatrixChunky8"); 468 459 } 469 460 if (opponent) { 470 461 ink(200, 70, 60, 150).write(opponent.handle, { 471 - x: ox + Math.floor(opponent.x) - opponent.handle.length * 3, 472 - y: oy + Math.floor(opponent.y) + BODY_R + 5, 462 + x: ox + Math.floor(opponent.x) - Math.floor(opponent.handle.length * 2), 463 + y: oy + Math.floor(opponent.y) + 9, 473 464 }, undefined, undefined, false, "MatrixChunky8"); 474 465 } 475 466 ··· 492 483 }); 493 484 } 494 485 495 - // Update available banner 496 - if (updateAvailable) { 497 - ink(50, 160, 80).write("update ready", { 498 - x: ox, y: oy + ARENA_H + 16, 499 - }); 500 - ink(140, 135, 125).write("tap here to reload", { 501 - x: ox, y: oy + ARENA_H + 26, 502 - }); 503 - } 504 - 505 486 // Stack 506 487 const stackX = ox + ARENA_W + 8; 507 488 const stackY = oy; ··· 519 500 } 520 501 } 521 502 522 - function drawFigure(ink, circle, box, line, ox, oy, fig, col) { 503 + function drawFigure(ink, circle, box, line, ox, oy, fig, col, fc) { 523 504 const fx = ox + Math.floor(fig.x); 524 505 const fy = oy + Math.floor(fig.y); 525 506 526 507 if (!fig.alive) { 527 - // Dead — X mark 528 508 ink(col[0], col[1], col[2], 60); 529 509 line(fx - 3, fy - 3, fx + 3, fy + 3); 530 510 line(fx + 3, fy - 3, fx - 3, fy + 3); 531 511 return; 532 512 } 533 513 514 + // Leg animation based on movement 515 + const dx = (fig.targetX || fig.x) - fig.x; 516 + const dy = (fig.targetY || fig.y) - fig.y; 517 + const moving = dx * dx + dy * dy > 4; 518 + const legSwing = moving ? Math.sin(fc * 0.3) * 3 : 0; 519 + 534 520 ink(...col); 535 - // Legs (splayed below body) 536 - line(fx, fy + 1, fx - 4, fy + 6); 537 - line(fx, fy + 1, fx + 4, fy + 6); 521 + // Legs 522 + line(fx, fy + 1, fx - 4 + legSwing, fy + 6); 523 + line(fx, fy + 1, fx + 4 - legSwing, fy + 6); 538 524 // Arms 539 - line(fx - 1, fy - 1, fx - 5, fy + 2); 540 - line(fx + 1, fy - 1, fx + 5, fy + 2); 541 - // Head (filled circle on top) 525 + line(fx - 1, fy - 1, fx - 5 - legSwing * 0.5, fy + 2); 526 + line(fx + 1, fy - 1, fx + 5 + legSwing * 0.5, fy + 2); 527 + // Head 542 528 circle(fx, fy - 2, 3, true); 543 529 // Eye dot 544 530 ink(255, 255, 255).box(fx, fy - 3, 1, 1);