Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

dumduel: fix practice mode, WS snapshot fallback, allow movement during countdown

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

+51 -32
+15 -4
session-server/duel-manager.mjs
··· 154 154 const player = this.players.get(handle); 155 155 if (!player || !player.alive) return; 156 156 if (!this.isDuelist(handle)) return; 157 - if (this.phase !== "fight") return; 157 + if (this.phase !== "fight" && this.phase !== "countdown") return; 158 158 159 159 player.targetX = Math.max(6, Math.min(ARENA_W - 6, input.targetX)); 160 160 player.targetY = Math.max(6, Math.min(ARENA_H - 6, input.targetY)); ··· 188 188 189 189 if (realPlayers.length === 0) { 190 190 this.resetToWaiting(); 191 + this.stopTick(); 191 192 return; 192 193 } 193 194 194 - if (realPlayers.length === 1 && this.phase === "waiting") { 195 - // Solo — start practice with dummy 195 + if (realPlayers.length === 1) { 196 + // Solo — start practice with dummy (regardless of current phase) 197 + // Remove dummy first if stale 198 + if (this.roster.includes(DUMMY_HANDLE)) { 199 + this.roster = this.roster.filter((h) => h !== DUMMY_HANDLE); 200 + this.players.delete(DUMMY_HANDLE); 201 + } 202 + this.bullets = []; 203 + this.phase = "waiting"; // reset phase so startPractice works 196 204 this.startPractice(realPlayers[0]); 197 205 return; 198 206 } ··· 204 212 this.bullets = []; 205 213 } 206 214 207 - if (this.roster.length >= 2 && this.phase === "waiting") { 215 + if (this.roster.length >= 2 && (this.phase === "waiting" || this.phase === "roundover")) { 208 216 this.startCountdown(); 209 217 } 210 218 } ··· 262 270 timer: this.countdownTimer, 263 271 }); 264 272 273 + console.log(`🎯 Duel countdown: ${duelists.join(" vs ")} (phase: ${this.phase})`); 265 274 this.ensureTick(); 266 275 } 267 276 ··· 348 357 349 358 if (this.phase === "countdown") { 350 359 this.countdownTimer--; 360 + this.tickDummy(); 361 + this.tickMovement(); 351 362 if (this.countdownTimer <= 0) this.startFight(); 352 363 } 353 364
+36 -28
system/public/aesthetic.computer/disks/dumduel.mjs
··· 31 31 let roundWinner = null; 32 32 let ping = 0; 33 33 34 + function applySnapshot(s) { 35 + snap = s; 36 + phase = s.phase; 37 + countdownTimer = s.countdownTimer; 38 + roundWinner = s.roundWinner; 39 + roster = (s.roster || []).map((h) => ({ handle: h })); 40 + 41 + // Reconcile prediction 42 + const myAck = s.lastInputSeq?.[myHandle] || 0; 43 + pendingInputs = pendingInputs.filter((inp) => inp.seq > myAck); 44 + 45 + const meServer = s.players?.find((p) => p.handle === myHandle); 46 + if (meServer) { 47 + localX = meServer.x; 48 + localY = meServer.y; 49 + localTargetX = meServer.targetX; 50 + localTargetY = meServer.targetY; 51 + 52 + for (const inp of pendingInputs) { 53 + localTargetX = inp.targetX; 54 + localTargetY = inp.targetY; 55 + } 56 + 57 + ping = meServer.ping || 0; 58 + } 59 + } 60 + 34 61 function boot({ wipe, screen, net: { socket, udp }, handle, sound, send }) { 35 62 sw = screen.width; 36 63 sh = screen.height; ··· 65 92 udpChannel = udp((type, content) => { 66 93 if (type === "duel:snapshot") { 67 94 const s = typeof content === "string" ? JSON.parse(content) : content; 68 - snap = s; 69 - phase = s.phase; 70 - countdownTimer = s.countdownTimer; 71 - roundWinner = s.roundWinner; 72 - roster = (s.roster || []).map((h) => ({ handle: h })); 73 - 74 - // Reconcile prediction 75 - const myAck = s.lastInputSeq?.[myHandle] || 0; 76 - pendingInputs = pendingInputs.filter((inp) => inp.seq > myAck); 77 - 78 - const meServer = s.players?.find((p) => p.handle === myHandle); 79 - if (meServer) { 80 - // Start from server position 81 - localX = meServer.x; 82 - localY = meServer.y; 83 - localTargetX = meServer.targetX; 84 - localTargetY = meServer.targetY; 85 - 86 - // Re-apply unacknowledged inputs 87 - for (const inp of pendingInputs) { 88 - localTargetX = inp.targetX; 89 - localTargetY = inp.targetY; 90 - } 91 - 92 - ping = meServer.ping || 0; 93 - } 95 + applySnapshot(s); 94 96 } 95 97 }); 96 98 ··· 102 104 } 103 105 104 106 const msg = typeof content === "string" ? JSON.parse(content) : content; 107 + 108 + // Snapshot fallback via WS (when UDP not available) 109 + if (type === "duel:snapshot") { 110 + applySnapshot(msg); 111 + return; 112 + } 105 113 106 114 if (type === "duel:joined" || type === "duel:roster") { 107 115 roster = (msg.roster || []).map((h) => ({ handle: h })); ··· 149 157 frameCount++; 150 158 151 159 // Predict local movement 152 - if (phase === "fight") { 160 + if (phase === "fight" || phase === "countdown") { 153 161 const dx = localTargetX - localX; 154 162 const dy = localTargetY - localY; 155 163 const dist = Math.sqrt(dx * dx + dy * dy); ··· 174 182 sw = screen.width; 175 183 sh = screen.height; 176 184 177 - if (e.is("touch") && phase === "fight") { 185 + if (e.is("touch") && (phase === "fight" || phase === "countdown")) { 178 186 const ox = Math.floor(sw / 2 - ARENA_W / 2); 179 187 const oy = Math.floor(sh / 2 - ARENA_H / 2); 180 188 const tx = Math.max(6, Math.min(ARENA_W - 6, e.x - ox));