Skip to content

Commit e36561d

Browse files
authored
Merge pull request #77 from sam-mfb/bugfix
Bugfix
2 parents 1475ae1 + aa08901 commit e36561d

File tree

16 files changed

+53
-63
lines changed

16 files changed

+53
-63
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changes
22

3+
## 1.3.1 - 2025.11.01
4+
5+
**BREAKING CHANGE - Game Engine Version 2**
6+
7+
Fix bug with number of starting lives - games now correctly start with 3 lives (SHIPSTART + 1) instead of 2. This is a breaking change for validation: recordings made with version 1 will fail validation as they have incorrect initial state.
8+
39
## 1.3.0 - 2025.10.26
410

511
Game recording, validation, and replay

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "continuum",
3-
"version": "1.3.0",
3+
"version": "1.3.1",
44
"type": "module",
55
"description": "Recreation of the 68000 Mac game Continuum for the web",
66
"scripts": {

scripts/validate-recording.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,14 @@ const main = async (): Promise<void> => {
7575
}
7676

7777
// Create headless store (no UI, sound, or persistence)
78-
const store = createHeadlessStore(
79-
services,
80-
recording.initialState.lives,
81-
recording.startLevel
82-
)
78+
const store = createHeadlessStore(services, recording.startLevel)
8379

8480
// Create headless engine (uses frame counter instead of real fizz service)
8581
const engine = createHeadlessGameEngine(
8682
store,
8783
galaxyService,
8884
randomService,
89-
recording.galaxyId,
90-
recording.initialState.lives
85+
recording.galaxyId
9186
)
9287

9388
const validator = createRecordingValidator(engine, store, recordingService)

src/core/game/levelThunks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { statusSlice } from '@core/status'
1212
import { clearAllShots } from '@core/shots'
1313
import { resetSparksAlive, clearShards } from '@core/explosions'
1414
import { SCRWTH, TOPMARG, BOTMARG } from '@core/screen'
15-
import { clearLevelComplete } from './gameSlice'
15+
import { clearLevelComplete, resetKillShipNextFrame } from './gameSlice'
1616
import { setMessage } from '@/core/status'
1717
import { FUELFRAMES } from '@core/figs'
1818

@@ -59,8 +59,9 @@ export const loadLevel =
5959
// Update the current level in status state to match what we're loading
6060
dispatch(statusSlice.actions.setLevel(levelNum))
6161

62-
// Reset level complete flag and status message for the new level
62+
// Reset level complete flag, kill ship flag, and status message for the new level
6363
dispatch(clearLevelComplete())
64+
dispatch(resetKillShipNextFrame())
6465
dispatch(setMessage(null))
6566

6667
// Get the planet data for this level from the service

src/core/ship/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const SKILLBRADIUS = 30 // Radius for bunker death blast when ship dies (
44
export const SCOREBUNK = 50 // Points for destroying bunker
55
export const FUELSTART = 10000 // Starting amount of fuel (GW.h:136)
66
export const SHIPSTART = 2 // Number of spare ships to start with (GW.h:135)
7+
export const TOTAL_INITIAL_LIVES = SHIPSTART + 1 // Total lives including current ship
78

89
// Shield-related constants from GW.h
910
export const FUELBURN = 16 // Fuel consumed per frame while thrusting (GW.h:138)

src/core/ship/shipSlice.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
22
import type { ShipState } from './types'
3-
import {
4-
SHIP,
5-
DEAD_TIME,
6-
FUELSTART,
7-
FUELGAIN,
8-
FUELBURN,
9-
SHIPSTART
10-
} from './constants'
3+
import { SHIP, DEAD_TIME, FUELSTART, FUELGAIN, FUELBURN } from './constants'
114
import type { ControlMatrix } from '../controls'
125

136
// Note: TOTAL_INITIAL_LIVES will be set via preloadedState when creating the store
@@ -362,8 +355,8 @@ export const shipSlice = createSlice({
362355
/**
363356
* Reset lives for new game
364357
*/
365-
resetLives: state => {
366-
state.lives = SHIPSTART
358+
resetLives: (state, action: PayloadAction<number>) => {
359+
state.lives = action.payload
367360
},
368361

369362
/**

src/core/validation/HeadlessGameEngine.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { GalaxyService } from '@core/galaxy'
66
import type { RandomService } from '@/core/shared'
77
import { updateGameState } from '@core/game'
88
import { FIZZ_DURATION } from '@core/transition'
9+
import { TOTAL_INITIAL_LIVES } from '@core/ship'
910

1011
type HeadlessGameEngine = {
1112
step: (frameCount: number, controls: ControlMatrix) => void
@@ -16,8 +17,7 @@ const createHeadlessGameEngine = (
1617
store: HeadlessStore,
1718
galaxyService: GalaxyService,
1819
randomService: RandomService,
19-
galaxyId: string,
20-
initialLives: number
20+
galaxyId: string
2121
): HeadlessGameEngine => {
2222
// Track fizz state to simulate correct duration in headless mode
2323
let fizzFramesElapsed = 0
@@ -48,7 +48,7 @@ const createHeadlessGameEngine = (
4848
capturedFinalState = finalState
4949
},
5050
getGalaxyId: (): string => galaxyId,
51-
getInitialLives: (): number => initialLives,
51+
getInitialLives: (): number => TOTAL_INITIAL_LIVES,
5252
getCollisionMode: (): 'original' | 'modern' => 'modern' // Headless validation uses modern collision
5353
}
5454

src/core/validation/createHeadlessStore.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { combineSlices, configureStore } from '@reduxjs/toolkit'
22
import { gameSlice } from '@core/game'
3-
import { shipSlice } from '@core/ship'
3+
import { shipSlice, TOTAL_INITIAL_LIVES } from '@core/ship'
44
import { shotsSlice } from '@core/shots'
55
import { planetSlice } from '@core/planet'
66
import { screenSlice } from '@core/screen'
@@ -40,31 +40,15 @@ const headlessReducer = combineSlices(
4040

4141
const createHeadlessStore = (
4242
services: HeadlessServices,
43-
initialLives: number,
4443
startLevel: number
4544
): ReturnType<typeof configureStore<GameRootState>> => {
46-
const preloadedState = {
47-
ship: {
48-
...shipSlice.getInitialState(),
49-
lives: initialLives
50-
},
51-
status: {
52-
...statusSlice.getInitialState(),
53-
currentlevel: startLevel
54-
},
55-
game: {
56-
...gameSlice.getInitialState(),
57-
cheatUsed: startLevel > 1 // Mark cheat used if starting beyond level 1
58-
}
59-
}
60-
6145
// Create the sync thunk middleware instance
6246
const syncThunkMiddleware = createSyncThunkMiddleware<
6347
GameRootState,
6448
HeadlessServices
6549
>()
6650

67-
return configureStore({
51+
const store = configureStore({
6852
reducer: headlessReducer,
6953
middleware: getDefaultMiddleware =>
7054
getDefaultMiddleware({
@@ -74,9 +58,23 @@ const createHeadlessStore = (
7458
// Disable serialization checks for headless validation
7559
// (randomService functions are passed in actions but that's okay for validation)
7660
serializableCheck: false
77-
}).prepend(syncThunkMiddleware(services)),
78-
preloadedState
61+
}).prepend(syncThunkMiddleware(services))
7962
})
63+
64+
// Initialize state using the same actions as replay and game
65+
// (ReplaySelectionScreen.tsx:132-136 and App.tsx:186-189)
66+
// This ensures validator state matches game/replay state exactly
67+
store.dispatch(shipSlice.actions.resetShip())
68+
store.dispatch(shipSlice.actions.resetFuel())
69+
store.dispatch(statusSlice.actions.initStatus(startLevel))
70+
store.dispatch(shipSlice.actions.resetLives(TOTAL_INITIAL_LIVES))
71+
72+
// Mark cheat used if starting beyond level 1
73+
if (startLevel > 1) {
74+
store.dispatch(gameSlice.actions.markCheatUsed())
75+
}
76+
77+
return store
8078
}
8179

8280
export type HeadlessStore = ReturnType<typeof createHeadlessStore>

src/game/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from './appSlice'
1919
import { isTouchDevice } from './mobile/deviceDetection'
2020
import { setHighScore } from '@/core/highscore'
21-
import { shipSlice } from '@/core/ship'
21+
import { shipSlice, TOTAL_INITIAL_LIVES } from '@/core/ship'
2222
import { statusSlice } from '@/core/status'
2323
import { markCheatUsed } from '@core/game'
2424
import { clearExplosions } from '@/core/explosions'
@@ -182,7 +182,7 @@ export const App: React.FC<AppProps> = ({
182182
onStartGame={(level: number) => {
183183
// Reset ship and sound to clean state
184184
dispatch(shipSlice.actions.resetShip())
185-
dispatch(shipSlice.actions.resetLives())
185+
dispatch(shipSlice.actions.resetLives(TOTAL_INITIAL_LIVES))
186186
dispatch(shipSlice.actions.resetFuel())
187187

188188
// Reset score and status for new game

0 commit comments

Comments
 (0)