11import { DurableObject } from 'cloudflare:workers' ;
22import type { GameState , C2S , S2C , AstrogationOrder , OrdnanceLaunch , CombatAttack } from '../shared/types' ;
33import { getSolarSystemMap , SCENARIOS , findBaseHex } from '../shared/map-data' ;
4- import { INACTIVITY_TIMEOUT_MS } from '../shared/constants' ;
4+ import { INACTIVITY_TIMEOUT_MS , TURN_TIMEOUT_MS } from '../shared/constants' ;
55import { createGame , processAstrogation , processOrdnance , skipOrdnance , processCombat , skipCombat } from '../shared/game-engine' ;
66
77export interface Env {
@@ -188,13 +188,71 @@ export class GameDO extends DurableObject {
188188 return ;
189189 }
190190
191+ // Check if turn timeout is pending
192+ const turnTimeoutAt = await this . ctx . storage . get < number > ( 'turnTimeoutAt' ) ;
193+ if ( turnTimeoutAt !== undefined && Date . now ( ) >= turnTimeoutAt - 500 ) {
194+ await this . handleTurnTimeout ( ) ;
195+ return ;
196+ }
197+
191198 // Inactivity timeout — close everything
192199 for ( const ws of this . ctx . getWebSockets ( ) ) {
193200 try { ws . close ( 1000 , 'Inactivity timeout' ) ; } catch { }
194201 }
195202 await this . ctx . storage . deleteAll ( ) ;
196203 }
197204
205+ private async handleTurnTimeout ( ) : Promise < void > {
206+ await this . ctx . storage . delete ( 'turnTimeoutAt' ) ;
207+ const gameState = await this . getGameState ( ) ;
208+ if ( ! gameState || gameState . phase === 'gameOver' ) return ;
209+
210+ const map = getSolarSystemMap ( ) ;
211+ const playerId = gameState . activePlayer ;
212+
213+ if ( gameState . phase === 'astrogation' ) {
214+ // Auto-submit empty orders (no burns)
215+ const orders : AstrogationOrder [ ] = gameState . ships
216+ . filter ( s => s . owner === playerId )
217+ . map ( s => ( { shipId : s . id , burn : null } ) ) ;
218+ const result = processAstrogation ( gameState , playerId , orders , map ) ;
219+ if ( ! ( 'error' in result ) ) {
220+ this . broadcast ( { type : 'movementResult' , movements : result . movements , ordnanceMovements : result . ordnanceMovements , events : result . events , state : result . state } ) ;
221+ this . broadcastEndOrUpdate ( result . state ) ;
222+ await this . saveGameState ( result . state ) ;
223+ await this . startTurnTimer ( result . state ) ;
224+ }
225+ } else if ( gameState . phase === 'ordnance' ) {
226+ const result = skipOrdnance ( gameState , playerId ) ;
227+ if ( ! ( 'error' in result ) ) {
228+ this . broadcast ( { type : 'stateUpdate' , state : result . state } ) ;
229+ this . broadcastEndOrUpdate ( result . state ) ;
230+ await this . saveGameState ( result . state ) ;
231+ await this . startTurnTimer ( result . state ) ;
232+ }
233+ } else if ( gameState . phase === 'combat' ) {
234+ const result = skipCombat ( gameState , playerId , map ) ;
235+ if ( ! ( 'error' in result ) ) {
236+ if ( result . baseDefenseResults && result . baseDefenseResults . length > 0 ) {
237+ this . broadcast ( { type : 'combatResult' , results : result . baseDefenseResults , state : result . state } ) ;
238+ }
239+ this . broadcastEndOrUpdate ( result . state ) ;
240+ await this . saveGameState ( result . state ) ;
241+ await this . startTurnTimer ( result . state ) ;
242+ }
243+ }
244+ }
245+
246+ private async startTurnTimer ( state : GameState ) : Promise < void > {
247+ if ( state . phase === 'gameOver' ) {
248+ await this . ctx . storage . delete ( 'turnTimeoutAt' ) ;
249+ return ;
250+ }
251+ const timeoutAt = Date . now ( ) + TURN_TIMEOUT_MS ;
252+ await this . ctx . storage . put ( 'turnTimeoutAt' , timeoutAt ) ;
253+ await this . ctx . storage . setAlarm ( timeoutAt ) ;
254+ }
255+
198256 // --- Game logic (delegates to engine) ---
199257
200258 private async initGame ( ) {
@@ -207,6 +265,7 @@ export class GameDO extends DurableObject {
207265
208266 await this . saveGameState ( gameState ) ;
209267 this . broadcast ( { type : 'gameStart' , state : gameState } ) ;
268+ await this . startTurnTimer ( gameState ) ;
210269 }
211270
212271 private async handleAstrogation ( playerId : number , ws : WebSocket , orders : AstrogationOrder [ ] ) {
@@ -224,6 +283,7 @@ export class GameDO extends DurableObject {
224283 this . broadcast ( { type : 'movementResult' , movements : result . movements , ordnanceMovements : result . ordnanceMovements , events : result . events , state : result . state } ) ;
225284 this . broadcastEndOrUpdate ( result . state ) ;
226285 await this . saveGameState ( result . state ) ;
286+ await this . startTurnTimer ( result . state ) ;
227287 }
228288
229289 private async handleOrdnance ( playerId : number , ws : WebSocket , launches : OrdnanceLaunch [ ] ) {
@@ -241,6 +301,7 @@ export class GameDO extends DurableObject {
241301 this . broadcast ( { type : 'stateUpdate' , state : result . state } ) ;
242302 this . broadcastEndOrUpdate ( result . state ) ;
243303 await this . saveGameState ( result . state ) ;
304+ await this . startTurnTimer ( result . state ) ;
244305 }
245306
246307 private async handleSkipOrdnance ( playerId : number , ws : WebSocket ) {
@@ -256,6 +317,7 @@ export class GameDO extends DurableObject {
256317
257318 this . broadcast ( { type : 'stateUpdate' , state : result . state } ) ;
258319 await this . saveGameState ( result . state ) ;
320+ await this . startTurnTimer ( result . state ) ;
259321 }
260322
261323 private async handleCombat ( playerId : number , ws : WebSocket , attacks : CombatAttack [ ] ) {
@@ -273,6 +335,7 @@ export class GameDO extends DurableObject {
273335 this . broadcast ( { type : 'combatResult' , results : result . results , state : result . state } ) ;
274336 this . broadcastEndOrUpdate ( result . state ) ;
275337 await this . saveGameState ( result . state ) ;
338+ await this . startTurnTimer ( result . state ) ;
276339 }
277340
278341 private async handleSkipCombat ( playerId : number , ws : WebSocket ) {
@@ -294,6 +357,7 @@ export class GameDO extends DurableObject {
294357
295358 this . broadcastEndOrUpdate ( result . state ) ;
296359 await this . saveGameState ( result . state ) ;
360+ await this . startTurnTimer ( result . state ) ;
297361 }
298362
299363 private async handleRematch ( playerId : number , ws : WebSocket ) {
0 commit comments