Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

arena: xbox 360 gamepad controls + on-screen controller minimap

Left stick drives movement, right stick looks (framerate-independent in sim),
A/B map to jump/crouch, D-pad zooms, Start respawns. A schematic minimap
appears top-left once any gamepad event fires, showing live stick + button
state.

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

+159
+159
system/public/aesthetic.computer/disks/arena.mjs
··· 49 49 let playerAlive = true; 50 50 let deathTickAge = 0; // how long we've been dead (sim ticks, for UI fade-in) 51 51 52 + // 🎮 Gamepad / Xbox controller state. 53 + // Populated lazily on the first gamepad event, then driven by act() each 54 + // frame. Connection persists for the session — we don't get disconnect events 55 + // surfaced through the disk event stream, so we treat "ever seen" as "still 56 + // here" for UI purposes. 57 + let gamepadState = { 58 + connected: false, 59 + id: null, 60 + index: 0, 61 + buttons: {}, // { 0: true, 1: false, ... } indexed by Standard Gamepad button 62 + axes: { 0: 0, 1: 0, 2: 0, 3: 0 }, 63 + // Mirrors what we last told doll.setMovement so we only emit on transitions. 64 + movement: { forward: false, back: false, left: false, right: false }, 65 + }; 66 + const GP_DEADZONE = 0.3; // movement threshold (gamepad.mjs already pre-filters at 0.15) 67 + const GP_LOOK_DEG_PER_SEC = 180; // right-stick look speed at full deflection 68 + const GP_PITCH_LIMIT = 89; 69 + 52 70 // 🟨 Per-tile highlight state. 53 71 // hoverTile: { row, col } of tile under the crosshair, or null. 54 72 // walkedTiles: Map<tileKey, ageTicks>. A tile just stepped onto starts at ··· 650 668 } 651 669 } 652 670 671 + // 🎮 Right-stick → camera look. Integrate continuously in sim so the look 672 + // speed is framerate-independent. Skip while orbiting (right-mouse drag) so 673 + // the two camera-control schemes don't fight. 674 + if (gamepadState.connected && !orbiting) { 675 + const rx = gamepadState.axes[2] || 0; 676 + const ry = gamepadState.axes[3] || 0; 677 + if (rx !== 0 || ry !== 0) { 678 + const dt = 1 / SIM_HZ; 679 + cam.rotY += rx * GP_LOOK_DEG_PER_SEC * dt; 680 + cam.rotX = Math.max( 681 + -GP_PITCH_LIMIT, 682 + Math.min(GP_PITCH_LIMIT, cam.rotX + ry * GP_LOOK_DEG_PER_SEC * dt), 683 + ); 684 + } 685 + } 686 + 653 687 // Undo any orbit offset we applied last frame so it doesn't affect physics 654 688 cam.x -= appliedOrbitOffset[0]; 655 689 cam.z -= appliedOrbitOffset[1]; ··· 1101 1135 } 1102 1136 } 1103 1137 1138 + // 🎮 Controller minimap — appears top-left whenever a gamepad has been 1139 + // detected this session. Schematic Xbox-style layout with live stick + button 1140 + // state so the player can verify input is reaching the piece. 1141 + if (gamepadState.connected) { 1142 + const px = 4, py = 4, w = 90, h = 56; 1143 + const btn = (i) => !!gamepadState.buttons[i]; 1144 + 1145 + // Body 1146 + ink(20, 25, 35, 210).box(px, py, w, h, "fill"); 1147 + ink(80, 100, 130, 230).box(px, py, w, h, "outline"); 1148 + 1149 + // Triggers (LT=6, RT=7) — short bars across the top edge 1150 + ink(btn(6) ? "yellow" : [70, 80, 100]).box(px + 4, py + 2, 18, 3, "fill"); 1151 + ink(btn(7) ? "yellow" : [70, 80, 100]).box(px + w - 22, py + 2, 18, 3, "fill"); 1152 + // Bumpers (LB=4, RB=5) — bars just below the triggers 1153 + ink(btn(4) ? "white" : [70, 80, 100]).box(px + 4, py + 7, 18, 3, "fill"); 1154 + ink(btn(5) ? "white" : [70, 80, 100]).box(px + w - 22, py + 7, 18, 3, "fill"); 1155 + 1156 + // Left stick well + dot (axes 0, 1; LS press = button 10) 1157 + const lsx = px + 14, lsy = py + 26, lsr = 8; 1158 + ink(40, 50, 70, 230).circle(lsx, lsy, lsr, true); 1159 + ink(110, 130, 160).circle(lsx, lsy, lsr); 1160 + const lsDx = (gamepadState.axes[0] || 0) * (lsr - 2); 1161 + const lsDy = (gamepadState.axes[1] || 0) * (lsr - 2); 1162 + ink(btn(10) ? "yellow" : "white").circle(lsx + lsDx, lsy + lsDy, 2, true); 1163 + 1164 + // Right stick (axes 2, 3; RS press = button 11) 1165 + const rsx = px + w - 14, rsy = py + 26, rsr = 8; 1166 + ink(40, 50, 70, 230).circle(rsx, rsy, rsr, true); 1167 + ink(110, 130, 160).circle(rsx, rsy, rsr); 1168 + const rsDx = (gamepadState.axes[2] || 0) * (rsr - 2); 1169 + const rsDy = (gamepadState.axes[3] || 0) * (rsr - 2); 1170 + ink(btn(11) ? "yellow" : "white").circle(rsx + rsDx, rsy + rsDy, 2, true); 1171 + 1172 + // D-pad cross (12=up, 13=down, 14=left, 15=right) 1173 + const dpx = px + 30, dpy = py + 38; 1174 + const dpOff = [70, 80, 100], dpOn = [255, 255, 255]; 1175 + ink(...(btn(12) ? dpOn : dpOff)).box(dpx, dpy - 4, 4, 4, "fill"); 1176 + ink(...(btn(13) ? dpOn : dpOff)).box(dpx, dpy + 4, 4, 4, "fill"); 1177 + ink(...(btn(14) ? dpOn : dpOff)).box(dpx - 4, dpy, 4, 4, "fill"); 1178 + ink(...(btn(15) ? dpOn : dpOff)).box(dpx + 4, dpy, 4, 4, "fill"); 1179 + 1180 + // Face buttons diamond (Y top, X left, B right, A bottom — Xbox colors) 1181 + const fbx = px + w - 30, fby = py + 38; 1182 + const face = (cx, cy, color, on) => { 1183 + ink(color[0], color[1], color[2], on ? 255 : 90).circle(cx, cy, 3, true); 1184 + ink(255, 255, 255, on ? 255 : 80).circle(cx, cy, 3); 1185 + }; 1186 + face(fbx, fby + 5, [60, 200, 80], btn(0)); // A — green 1187 + face(fbx + 5, fby, [220, 60, 60], btn(1)); // B — red 1188 + face(fbx - 5, fby, [60, 130, 220], btn(2)); // X — blue 1189 + face(fbx, fby - 5, [240, 220, 60], btn(3)); // Y — yellow 1190 + 1191 + // Back (8) / Start (9) / Guide (16) — tiny center dots 1192 + const cmx = px + w / 2, cmy = py + 26; 1193 + ink(...(btn(8) ? [255, 255, 255] : [100, 110, 130])).box(cmx - 7, cmy, 3, 3, "fill"); 1194 + ink(...(btn(9) ? [255, 255, 255] : [100, 110, 130])).box(cmx + 4, cmy, 3, 3, "fill"); 1195 + if (btn(16)) ink("lime").circle(cmx + 1, cmy + 9, 2, true); 1196 + 1197 + // Controller id (truncated) 1198 + const idShort = (gamepadState.id || "GAMEPAD").slice(0, 18).toUpperCase(); 1199 + ink(180, 200, 230, 200); 1200 + write(idShort, { x: px + 3, y: py + h - 9 }, undefined, undefined, false, font); 1201 + } 1202 + 1104 1203 // 📱 Draw mobile control buttons 1105 1204 if (mobileButtons) { 1106 1205 // Check if keyboard key is held for each button ··· 1137 1236 function act({ event: e, penLock, system }) { 1138 1237 if (e.is("pen:locked")) penLocked = true; 1139 1238 if (e.is("pen:unlocked")) penLocked = false; 1239 + 1240 + // 🎮 Gamepad — Standard Gamepad mapping (Xbox 360/One/Series, PS, etc.). 1241 + // Events arrive as `gamepad:<idx>:button:<n>:push|release` and 1242 + // `gamepad:<idx>:axis:<n>:move` from lib/gamepad.mjs. Parse the name once 1243 + // and dispatch by kind so we don't have to enumerate every button. 1244 + if (e.name && e.name.startsWith("gamepad:")) { 1245 + const m = e.name.match(/^gamepad:(\d+):(button|axis):(\d+):(push|release|move)$/); 1246 + if (m) { 1247 + const gi = +m[1], kind = m[2], idx = +m[3], action = m[4]; 1248 + const doll = system?.fps?.doll; 1249 + if (!gamepadState.connected) { 1250 + gamepadState.connected = true; 1251 + gamepadState.id = e.gamepadId || "Gamepad"; 1252 + gamepadState.index = gi; 1253 + } 1254 + if (kind === "button") { 1255 + const pushed = action === "push"; 1256 + gamepadState.buttons[idx] = pushed; 1257 + if (idx === 0 && doll) doll.setMovement("jump", pushed); // A → jump 1258 + if (idx === 1 && doll) doll.setMovement("crouch", pushed); // B → crouch 1259 + if (idx === 12 && pushed) { // D-pad up → zoom in 1260 + zoomLevel = Math.max(0, zoomLevel - 1); 1261 + applyZoom(doll); 1262 + } 1263 + if (idx === 13 && pushed) { // D-pad down → zoom out 1264 + zoomLevel = Math.min(ZOOM_DISTANCES.length - 1, zoomLevel + 1); 1265 + applyZoom(doll); 1266 + } 1267 + if (idx === 9 && pushed && !playerAlive && deathTickAge > 30) { // Start → respawn 1268 + playerAlive = true; 1269 + deathTickAge = 0; 1270 + walkedTiles.clear(); 1271 + prevPlayerTile = null; 1272 + doll?.respawn?.(0, 0); 1273 + } 1274 + } else if (kind === "axis") { 1275 + gamepadState.axes[idx] = e.value; 1276 + if ((idx === 0 || idx === 1) && doll) { 1277 + // Left stick → discrete forward/back/left/right with deadzone, so 1278 + // it slots into the same setMovement boolean flags the keyboard / 1279 + // mobile buttons drive. Any move past GP_DEADZONE counts as held. 1280 + const x = gamepadState.axes[0] || 0; 1281 + const y = gamepadState.axes[1] || 0; 1282 + const want = { 1283 + left: x < -GP_DEADZONE, 1284 + right: x > GP_DEADZONE, 1285 + forward: y < -GP_DEADZONE, 1286 + back: y > GP_DEADZONE, 1287 + }; 1288 + for (const k of ["left", "right", "forward", "back"]) { 1289 + if (want[k] !== gamepadState.movement[k]) { 1290 + doll.setMovement(k, want[k]); 1291 + gamepadState.movement[k] = want[k]; 1292 + } 1293 + } 1294 + } 1295 + // Axes 2/3 (right stick) are read in sim() for smooth look integration. 1296 + } 1297 + } 1298 + } 1140 1299 1141 1300 // ⌨️ Track keyboard state for visual feedback on buttons 1142 1301 if (e.is("keyboard:down")) {