Skip to content

Commit de841df

Browse files
committed
feat(objectives): ✨ implement state restoration from checkpoints
1 parent f496194 commit de841df

6 files changed

Lines changed: 147 additions & 13 deletions

File tree

src/engine/ui/draggable-box.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { EventBus } from '@app/events/event-bus';
2+
import { Events } from '@app/events/events';
13
import Draggabilly from 'draggabilly';
24
import { html } from '../utils/development/formatter';
35
import { getEl, showEl } from '../utils/get-el';
@@ -60,6 +62,10 @@ export abstract class DraggableBox {
6062
</div>
6163
`);
6264
}
65+
66+
EventBus.getInstance().on(Events.ROUTE_CHANGED, () => {
67+
this.close();
68+
});
6369
}
6470

6571
protected abstract getBoxContentHtml(): string;

src/objectives/objectives-manager.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,57 @@ export class ObjectivesManager {
114114
return (performance.now() - this.startTime_) / 1000; // Convert to seconds
115115
}
116116

117+
/**
118+
* Restore objective states from saved checkpoint data
119+
* Merges saved state with current objective definitions, preserving progress
120+
*/
121+
restoreState(savedStates: ObjectiveState[]): void {
122+
if (!savedStates || savedStates.length === 0) {
123+
return;
124+
}
125+
126+
// Create a map of saved states by objective ID for quick lookup
127+
const savedStateMap = new Map<string, ObjectiveState>();
128+
savedStates.forEach((state) => {
129+
savedStateMap.set(state.objective.id, state);
130+
});
131+
132+
// Restore state for each current objective
133+
for (const currentState of this.objectiveStates_) {
134+
const savedState = savedStateMap.get(currentState.objective.id);
135+
136+
// If no saved state for this objective, keep it as-is (fresh state)
137+
if (!savedState) {
138+
continue;
139+
}
140+
141+
// Restore activation state and timing
142+
currentState.isActive = savedState.isActive;
143+
currentState.activatedAt = savedState.activatedAt;
144+
currentState.isCompleted = savedState.isCompleted;
145+
currentState.completedAt = savedState.completedAt;
146+
147+
// Restore collapse state if objective was completed
148+
if (savedState.isCompleted) {
149+
this.collapsedObjectiveIds_.add(currentState.objective.id);
150+
}
151+
152+
// Restore condition states by index to ensure proper matching
153+
currentState.conditionStates.forEach((currentCondState, condIndex) => {
154+
const savedCondState = savedState.conditionStates[condIndex];
155+
156+
// Only restore if condition exists in saved state
157+
if (savedCondState) {
158+
currentCondState.isSatisfied = savedCondState.isSatisfied;
159+
currentCondState.satisfiedAt = savedCondState.satisfiedAt;
160+
currentCondState.maintainedDuration = savedCondState.maintainedDuration;
161+
currentCondState.isMaintenanceComplete = savedCondState.isMaintenanceComplete;
162+
currentCondState.lostTimestamps = savedCondState.lostTimestamps || [];
163+
}
164+
});
165+
}
166+
}
167+
117168
/**
118169
* Capture current collapse states from the DOM before regenerating HTML
119170
* Should be called before generateHtmlChecklist() to preserve user preferences
@@ -648,7 +699,7 @@ export class ObjectivesManager {
648699

649700
const state = specA.state;
650701
const tolerance = condition.params.referenceLevelTolerance ?? 1; // Default ±1 dB
651-
const diff = Math.abs(state.maxAmplitude - condition.params.referenceLevel);
702+
const diff = Math.abs(state.referenceLevel - condition.params.referenceLevel);
652703

653704
return diff <= tolerance;
654705
}

src/pages/sandbox-page.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,27 @@ export class SandboxPage extends BasePage {
7272
this.progressSaveManager_ = new ProgressSaveManager();
7373
this.progressSaveManager_.initialize();
7474

75-
this.initEquipment_();
75+
// Initialize equipment and objectives asynchronously
76+
this.initializeAsync_();
77+
}
78+
79+
/**
80+
* Handle async initialization of equipment and objectives
81+
*/
82+
private async initializeAsync_(): Promise<void> {
83+
await this.initEquipment_();
7684
SimulationManager.getInstance();
7785

7886
// Initialize objectives manager if scenario has objectives
7987
const scenario = ScenarioManager.getInstance();
8088
if (scenario.data?.objectives && scenario.data.objectives.length > 0) {
8189
ObjectivesManager.initialize(scenario.data.objectives);
8290
SimulationManager.getInstance().objectivesManager = ObjectivesManager.getInstance();
91+
92+
// If we're continuing from a checkpoint, restore objective states
93+
if (this.navigationOptions_.continueFromCheckpoint) {
94+
await this.restoreObjectiveStatesFromCheckpoint_();
95+
}
8396
}
8497

8598
EventBus.getInstance().emit(Events.DOM_READY);
@@ -162,6 +175,31 @@ export class SandboxPage extends BasePage {
162175
}
163176
}
164177

178+
/**
179+
* Restore objective states from checkpoint after ObjectivesManager has been initialized
180+
*/
181+
private async restoreObjectiveStatesFromCheckpoint_(): Promise<void> {
182+
if (!this.progressSaveManager_) {
183+
return;
184+
}
185+
186+
try {
187+
const scenario = ScenarioManager.getInstance();
188+
const checkpoint = await this.progressSaveManager_.loadCheckpoint(scenario.data.id) as {
189+
state: AppState;
190+
};
191+
192+
if (checkpoint?.state?.objectiveStates) {
193+
const objectivesManager = ObjectivesManager.getInstance();
194+
objectivesManager.restoreState(checkpoint.state.objectiveStates);
195+
Logger.info('Objective states restored from checkpoint');
196+
}
197+
} catch (error) {
198+
Logger.error('Failed to restore objective states from checkpoint:', error);
199+
// Continue without restoring objectives - they'll start fresh
200+
}
201+
}
202+
165203
hide(): void {
166204
SandboxPage.destroy();
167205
}
@@ -174,8 +212,8 @@ export class SandboxPage extends BasePage {
174212
}
175213

176214
SandboxPage.instance_ = null;
177-
ObjectivesManager.destroy();
178215
SimulationManager.destroy();
216+
ObjectivesManager.destroy();
179217
EventBus.destroy();
180218
const container = getEl(SandboxPage.containerId);
181219
if (container) {

src/pages/sandbox/equipment.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Events } from "@app/events/events";
66
import { DraggableHtmlBox } from "@app/modal/draggable-html-box";
77
import { ObjectivesManager } from "@app/objectives";
88
import { ScenarioManager, SimulationSettings } from "@app/scenario-manager";
9+
import { SimulationManager } from "@app/simulation/simulation-manager";
910
import { html } from "../../engine/utils/development/formatter";
1011
import { Antenna } from '../../equipment/antenna/antenna';
1112
import { RealTimeSpectrumAnalyzer } from '../../equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer';
@@ -60,14 +61,16 @@ export class Equipment extends BaseElement {
6061
</div>
6162
</div>
6263
`;
63-
missionBriefBox: DraggableHtmlBox;
64-
checklistBox: DraggableHtmlBox;
6564

6665
constructor(settings: SimulationSettings) {
6766
super();
6867
this.html_ = settings.layout ? settings.layout : this.html_;
6968
this.init_(SandboxPage.containerId, 'replace');
7069
this.initEquipment_(settings);
70+
71+
EventBus.getInstance().on(Events.ROUTE_CHANGED, () => {
72+
this.stopChecklistRefreshTimer_();
73+
});
7174
}
7275

7376
protected addEventListeners_(): void {
@@ -79,8 +82,8 @@ export class Equipment extends BaseElement {
7982
const missionBriefUrl = ScenarioManager.getInstance().settings.missionBriefUrl;
8083
if (missionBriefUrl) {
8184
qs('.mission-brief-icon').addEventListener('click', () => {
82-
this.missionBriefBox ??= new DraggableHtmlBox('Mission Brief', 'mission-brief', missionBriefUrl);
83-
this.missionBriefBox.open();
85+
SimulationManager.getInstance().missionBriefBox ??= new DraggableHtmlBox('Mission Brief', 'mission-brief', missionBriefUrl);
86+
SimulationManager.getInstance().missionBriefBox.open();
8487
});
8588
}
8689
}
@@ -89,24 +92,24 @@ export class Equipment extends BaseElement {
8992
const missionBriefUrl = ScenarioManager.getInstance().settings.missionBriefUrl;
9093
if (missionBriefUrl) {
9194
qs('.checklist-icon').addEventListener('click', () => {
92-
this.checklistBox ??= new DraggableHtmlBox('Checklist', 'checklist', '');
95+
SimulationManager.getInstance().checklistBox ??= new DraggableHtmlBox('Checklist', 'checklist', '');
9396
const objectivesManager = ObjectivesManager.getInstance();
9497
objectivesManager.syncCollapsedStatesFromDOM();
9598
this.lastChecklistHtml_ = objectivesManager.generateHtmlChecklist();
96-
this.checklistBox.updateContent(this.lastChecklistHtml_);
97-
this.checklistBox.open();
98-
this.startChecklistRefreshTimer_(this.checklistBox);
99+
SimulationManager.getInstance().checklistBox.updateContent(this.lastChecklistHtml_);
100+
SimulationManager.getInstance().checklistBox.open();
101+
this.startChecklistRefreshTimer_(SimulationManager.getInstance().checklistBox);
99102
});
100103

101104
EventBus.getInstance().on(Events.OBJECTIVE_ACTIVATED, () => {
102105
// Can't update it until they open it for the first time
103-
if (!this.checklistBox) {
106+
if (!SimulationManager.getInstance().checklistBox) {
104107
return;
105108
}
106109

107110
const objectivesManager = ObjectivesManager.getInstance();
108111
this.lastChecklistHtml_ = objectivesManager.generateHtmlChecklist();
109-
this.checklistBox.updateContent(this.lastChecklistHtml_);
112+
SimulationManager.getInstance().checklistBox.updateContent(this.lastChecklistHtml_);
110113
});
111114
}
112115
}

src/simulation/simulation-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Satellite } from '@app/equipment/satellite/satellite';
22
import { EventBus } from '@app/events/event-bus';
33
import { Events } from '@app/events/events';
4+
import { DraggableHtmlBox } from '@app/modal/draggable-html-box';
45
import { ObjectivesManager } from '@app/objectives';
56
import { Equipment } from '@app/pages/sandbox/equipment';
67
import { ScenarioManager } from '@app/scenario-manager';
@@ -24,6 +25,9 @@ export class SimulationManager {
2425
progressSaveManager: ProgressSaveManager;
2526
userDataServices: UserDataService;
2627

28+
missionBriefBox?: DraggableHtmlBox;
29+
checklistBox?: DraggableHtmlBox;
30+
2731
private constructor() {
2832
this.progressSaveManager = new ProgressSaveManager();
2933

@@ -80,6 +84,8 @@ export class SimulationManager {
8084
}
8185

8286
static destroy(): void {
87+
SimulationManager.instance_?.checklistBox?.close();
88+
SimulationManager.instance_?.missionBriefBox?.close();
8389
SimulationManager.instance_ = null;
8490
}
8591
}

src/sync/sync-manager.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { RFFrontEndState } from '@app/equipment/rf-front-end/rf-front-end';
2+
import { ObjectiveState } from '@app/objectives';
23
import { AntennaState } from '../equipment/antenna/antenna';
34
import { RealTimeSpectrumAnalyzerState } from '../equipment/real-time-spectrum-analyzer/real-time-spectrum-analyzer';
45
import { ReceiverState } from '../equipment/receiver/receiver';
@@ -156,7 +157,22 @@ export class SyncManager {
156157
return { equipment: undefined };
157158
}
158159

160+
// Get objective states from SimulationManager if available
161+
let objectiveStates: ObjectiveState[] | undefined;
162+
try {
163+
const { SimulationManager } = require('../simulation/simulation-manager');
164+
const sim = SimulationManager.getInstance();
165+
if (sim?.objectivesManager) {
166+
objectiveStates = sim.objectivesManager.getObjectiveStates() as ObjectiveState[];
167+
}
168+
} catch (error) {
169+
// SimulationManager or ObjectivesManager not available yet - this is expected during initialization
170+
console.debug('ObjectivesManager not available when building state:', error);
171+
objectiveStates = undefined;
172+
}
173+
159174
return {
175+
objectiveStates,
160176
equipment: {
161177
spectrumAnalyzersState: this.equipment.spectrumAnalyzers.map(sa => sa.state),
162178
antennasState: this.equipment.antennas.map(a => a.state),
@@ -219,13 +235,27 @@ export class SyncManager {
219235
}
220236
});
221237
}
238+
239+
// Sync Objective States if available
240+
if (state.objectiveStates && state.objectiveStates.length > 0) {
241+
try {
242+
const { SimulationManager } = require('../simulation/simulation-manager');
243+
const sim = SimulationManager.getInstance();
244+
if (sim?.objectivesManager) {
245+
sim.objectivesManager.restoreState(state.objectiveStates);
246+
}
247+
} catch (error) {
248+
console.debug('ObjectivesManager not available when syncing from storage:', error);
249+
}
250+
}
222251
}
223252
}
224253

225254
/**
226255
* AppState interface (should match your existing type)
227256
*/
228257
export interface AppState {
258+
objectiveStates?: ObjectiveState[];
229259
equipment?: {
230260
spectrumAnalyzersState?: RealTimeSpectrumAnalyzerState[];
231261
rfFrontEndsState?: RFFrontEndState[];

0 commit comments

Comments
 (0)