Skip to content

Commit f2ebd7a

Browse files
committed
single save game feature
1 parent d0c431e commit f2ebd7a

File tree

9 files changed

+138
-14
lines changed

9 files changed

+138
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# Version history
22

3-
## [v0.6.7](https://github.com/LBALab/lba2remake/compare/v0.6.5...v0.6.6) Bugfixes
3+
## [v0.6.7](https://github.com/LBALab/lba2remake/compare/v0.6.6...v0.6.7) Bugfixes
44
_July 6th, 2025_
55

6+
#### Gameplay
7+
8+
- Single Save Game (continue menu option will load the last saved game)
9+
610
#### Replacement 3D models
711

812
- Added Franco's Island 3D models

src/data/translations.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"loading": "Loading",
44
"help": "Help",
55
"resumeGame": "Resume Game",
6+
"continueGame": "Continue Game",
67
"newGame": "New Game",
78
"loadGame": "Load Game",
89
"saveGame": "Save Game",
@@ -77,6 +78,7 @@
7778
"loading": "Chargement",
7879
"help": "Aide",
7980
"resumeGame": "Reprendre Partie",
81+
"continueGame": "Continuer la Partie",
8082
"newGame": "Nouvelle Partie",
8183
"loadGame": "Charger une Partie",
8284
"saveGame": "Sauver la Partie",

src/game/Game.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@ export default class Game {
129129
this.resetControlsState();
130130
}
131131

132+
continueState() {
133+
this._gameState = createGameState();
134+
const newState = localStorage.getItem('game_state');
135+
if (newState) {
136+
this._gameState = {
137+
...this._gameState,
138+
...JSON.parse(newState),
139+
};
140+
}
141+
this.resetControlsState();
142+
}
143+
132144
resetControlsState() {
133145
this.controlsState.controlVector.set(0, 0),
134146
this.controlsState.action = 0;

src/game/GameState.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ export interface MagicBallState {
2727
maxBounces: number;
2828
}
2929

30+
interface ActorPhysicsState {
31+
position: THREE.Vector3;
32+
orientation: THREE.Quaternion;
33+
temp: {
34+
position: THREE.Vector3;
35+
destination: THREE.Vector3;
36+
angle: number;
37+
destAngle: number;
38+
doorPosition?: [number, number, number];
39+
};
40+
carried: {
41+
position: THREE.Vector3;
42+
orientation: THREE.Quaternion;
43+
};
44+
}
45+
3046
export interface HeroState {
3147
behaviour: number;
3248
prevBehaviour: number;
@@ -45,12 +61,17 @@ export interface HeroState {
4561
magicball: MagicBallState;
4662
handStrength: number;
4763
lastValidPosTime: number;
48-
position: THREE.Vector3;
64+
physics: ActorPhysicsState;
4965
animState?: AnimStateJSON;
5066
}
5167

68+
interface SceneState {
69+
index: number;
70+
}
71+
5272
export interface GameState {
5373
config: GameConfig;
74+
scene: SceneState;
5475
hero: HeroState;
5576
// The actor index who is currently talking.
5677
actorTalking: number;
@@ -73,6 +94,9 @@ export function createGameState(): GameState {
7394
ambienceVolume: 0.2,
7495
positionalAudio: getParams().audio3d,
7596
}, getLanguageConfig()),
97+
scene: {
98+
index: 0,
99+
},
76100
hero: {
77101
behaviour: 0,
78102
prevBehaviour: 0,
@@ -87,12 +111,12 @@ export function createGameState(): GameState {
87111
clover: { boxes: 2, leafs: 1 },
88112
magicball: null,
89113
handStrength: 5, // LVL_0
90-
position: null,
91114
lastValidPosTime: 0,
92115
animState: null,
93116
inventorySlot: 0,
94117
usingItemId: -1,
95118
equippedItemId: -1,
119+
physics: null,
96120
},
97121
actorTalking: -1,
98122
flags: {
@@ -101,7 +125,7 @@ export function createGameState(): GameState {
101125
holomap: createHolomapFlags()
102126
},
103127
save(hero: Actor) {
104-
this.hero.position = hero.physics.position;
128+
this.hero.physics = hero.physics;
105129
this.hero.animState = hero.animState.toJSON();
106130
return JSON.stringify(omit(this, ['save', 'load', 'config']));
107131
},
@@ -110,9 +134,45 @@ export function createGameState(): GameState {
110134
if (!state) {
111135
return;
112136
}
113-
hero.physics.position.x = state.hero.position.x;
114-
hero.physics.position.y = state.hero.position.y;
115-
hero.physics.position.z = state.hero.position.z;
137+
hero.physics.position = new THREE.Vector3(
138+
state.hero.physics.position.x,
139+
state.hero.physics.position.y,
140+
state.hero.physics.position.z,
141+
);
142+
hero.physics.orientation = new THREE.Quaternion(
143+
state.hero.physics.orientation.x,
144+
state.hero.physics.orientation.y,
145+
state.hero.physics.orientation.z,
146+
state.hero.physics.orientation.w
147+
);
148+
// hero.threeObject.rotation.setFromQuaternion(state.hero.physics.orientation);
149+
// hero.physics.temp.position = new THREE.Vector3(
150+
// state.hero.physics.temp.position?.x || 0,
151+
// state.hero.physics.temp.position?.y || 0,
152+
// state.hero.physics.temp.position?.z || 0
153+
// );
154+
// hero.physics.temp.destination = new THREE.Vector3(
155+
// state.hero.physics.temp.destination?.x || 0,
156+
// state.hero.physics.temp.destination?.y || 0,
157+
// state.hero.physics.temp.destination?.z || 0
158+
// );
159+
hero.physics.temp.angle = state.hero.physics.temp.angle;
160+
hero.physics.temp.destAngle = state.hero.physics.temp.angle; // void turning
161+
hero.state.isTurning = true;
162+
// if (state.hero.physics.temp.doorPosition) {
163+
// hero.physics.temp.doorPosition = [
164+
// state.hero.physics.temp.doorPosition[0],
165+
// state.hero.physics.temp.doorPosition[1],
166+
// state.hero.physics.temp.doorPosition[2]
167+
// ];
168+
// } else {
169+
// hero.physics.temp.doorPosition = null;
170+
// }
171+
hero.physics.carried.position = new THREE.Vector3(
172+
state.hero.physics.carried.position.x,
173+
state.hero.physics.carried.position.y,
174+
state.hero.physics.carried.position.z
175+
);
116176
// Merge the current animState with the saved one, overwritting
117177
// things like the currentFrame etc. to ensure we e.g. continue to
118178
// fly the jetpack if we drown whilst using it.

src/model/anim/AnimState.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ export default class AnimState {
188188
}
189189

190190
setFromJSON(data: AnimStateJSON) {
191+
if (!data) {
192+
return;
193+
}
191194
this.interpolating = data.interpolating;
192195
this._hasEnded = data.hasEnded;
193196
this.reachedLastFrame = data.reachedLastFrame;

src/resources/parsers/anim.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { Resource } from '../load';
55
import { Anim, KeyFrame, BoneFrame, BoneframeCanFall } from '../../model/anim/types';
66

77
export const parseAnim = (resource: Resource, index: number) => {
8+
if (index < 0) {
9+
console.debug(`Anim index ${index} is invalid in this context.`);
10+
return null;
11+
}
812
const buffer = resource.getEntry(index);
913
const data = new DataView(buffer);
1014
const obj : Anim = {

src/ui/GameUI.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ interface GameUIProps {
3131
setUiState: (state: any, callback?: Function) => void;
3232
sharedState?: any;
3333
stateHandler?: any;
34-
showMenu: (inGameMenu?: boolean) => void;
34+
showMenu: (inGameMenu?: boolean, continueMenu?: boolean) => void;
3535
hideMenu: (wasPaused?: boolean) => void;
3636
}
3737

@@ -189,13 +189,45 @@ export default class GameUI extends React.Component<GameUIProps, GameUIState> {
189189
sceneManager.goto(0, true);
190190
}
191191

192+
saveGame() {
193+
const { game, sceneManager } = this.props;
194+
const state = game.getState();
195+
state.save(sceneManager.getScene().actors[0]);
196+
state.hero.animState = null; // Reset hero anim state
197+
state.scene.index = sceneManager.getScene().index;
198+
localStorage.setItem('game_state', JSON.stringify(state));
199+
game.setUiState({hasSaveGame: true});
200+
}
201+
202+
async continueGameScene() {
203+
const { game, sceneManager } = this.props;
204+
game.resume();
205+
game.continueState();
206+
const scene = await sceneManager.goto(game.getState().scene.index, true, false, true);
207+
game.getState().load(JSON.stringify(game.getState()), scene.actors[0]);
208+
}
209+
192210
onMenuItemChanged(item) {
193211
const { game } = this.props;
194212
switch (item) {
195213
case 70: { // Resume
196214
this.props.hideMenu();
197215
break;
198216
}
217+
case 73: { // Save Game
218+
if (game.isPaused()) {
219+
game.resume();
220+
}
221+
this.props.setUiState({showMenu: false, inGameMenu: true});
222+
this.saveGame();
223+
this.props.setUiState({hasSaveGame: true});
224+
break;
225+
}
226+
case 69: { // Continue
227+
this.props.hideMenu();
228+
this.continueGameScene();
229+
break;
230+
}
199231
case 71: { // New Game
200232
this.props.hideMenu();
201233
const onEnded = () => {
@@ -332,6 +364,7 @@ export default class GameUI extends React.Component<GameUIProps, GameUIState> {
332364
showMenu,
333365
teleportMenu,
334366
inGameMenu,
367+
hasSaveGame,
335368
loading,
336369
text,
337370
skip,
@@ -366,6 +399,7 @@ export default class GameUI extends React.Component<GameUIProps, GameUIState> {
366399
<Menu
367400
showMenu={showMenu && !teleportMenu}
368401
inGameMenu={inGameMenu}
402+
hasSaveGame={hasSaveGame}
369403
onItemChanged={this.onMenuItemChanged}
370404
/>
371405
{showMenu && !teleportMenu

src/ui/UIState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default interface UIState {
2424
menuTexts?: any;
2525
showMenu: boolean;
2626
inGameMenu: boolean;
27+
hasSaveGame: boolean;
2728
teleportMenu: boolean;
2829
behaviourMenu: boolean;
2930
inventory: boolean;
@@ -44,6 +45,7 @@ export function initUIState(game: Game): UIState {
4445
menuTexts: null,
4546
showMenu: false,
4647
inGameMenu: false,
48+
hasSaveGame: localStorage.getItem('game_state') !== null,
4749
teleportMenu: false,
4850
behaviourMenu: false,
4951
inventory: false,

src/ui/game/Menu.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ interface Item {
1515
text?: string;
1616
}
1717

18-
const getMenuItems = (resume: boolean = false): Item[] => {
18+
const getMenuItems = (resume: boolean = false, continueMenu: boolean = false): Item[] => {
1919
const params = getParams();
20+
const continuing = !resume && continueMenu;
2021
const menu = [
2122
{ item: 'ResumeGame', index: 70, isVisible: resume, isEnabled: true, textId: 'resumeGame' },
22-
{ item: 'NewGame', index: 71, isVisible: true, isEnabled: true, textId: 'newGame' },
23+
{ item: 'ContinueGame', index: 69, isVisible: continuing, isEnabled: true, textId: 'continueGame' },
2324
{ item: 'LoadGame', index: 72, isVisible: false, isEnabled: false, textId: 'loadGame' },
24-
{ item: 'SaveGame', index: 73, isVisible: false, isEnabled: false, textId: 'saveGame' },
25+
{ item: 'SaveGame', index: 73, isVisible: resume, isEnabled: true, textId: 'saveGame' },
26+
{ item: 'NewGame', index: 71, isVisible: !resume, isEnabled: true, textId: 'newGame' },
2527
{ item: 'Teleport', index: -1, isVisible: true, isEnabled: true, textId: 'teleport' },
2628
{ item: 'Editor', index: -2, isVisible: !params.editor, isEnabled: true, textId: 'editor' },
2729
{
@@ -60,6 +62,7 @@ const getOptionItems = (): Item[] => {
6062
interface MProps {
6163
showMenu: boolean;
6264
inGameMenu: boolean;
65+
hasSaveGame: boolean;
6366
onItemChanged: (id: number) => void;
6467
}
6568

@@ -76,7 +79,7 @@ export default class Menu extends React.Component<MProps, MState> {
7679
this.gamepadListener = this.gamepadListener.bind(this);
7780
this.state = {
7881
selectedIndex: 0,
79-
items: getMenuItems(false),
82+
items: getMenuItems(false, localStorage.getItem('game_state') !== null),
8083
};
8184
}
8285

@@ -86,7 +89,7 @@ export default class Menu extends React.Component<MProps, MState> {
8689
}
8790

8891
componentWillReceiveProps(newProps) {
89-
const menu = getMenuItems(newProps.inGameMenu);
92+
const menu = getMenuItems(newProps.inGameMenu, newProps.hasSaveGame);
9093
this.setState({ items: menu, selectedIndex: 0 });
9194
}
9295

@@ -138,7 +141,7 @@ export default class Menu extends React.Component<MProps, MState> {
138141
});
139142
} else if (this.state.items[selectedIndex].index === 741) {
140143
this.setState({
141-
items: getMenuItems(this.props.inGameMenu),
144+
items: getMenuItems(this.props.inGameMenu, this.props.hasSaveGame),
142145
selectedIndex: 0,
143146
});
144147
}

0 commit comments

Comments
 (0)