diff --git a/client/src/App.tsx b/client/src/App.tsx
index d5fb269a..a0852c87 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,17 +1,14 @@
import LiberationMap from "./components/liberationmap";
import useEventStream from "./hooks/useEventSteam";
import useInitialGameState from "./hooks/useInitialGameState";
-import { LatLng } from "leaflet";
function App() {
- const mapCenter: LatLng = new LatLng(25.58, 54.9);
-
useInitialGameState();
useEventStream();
return (
-
+
);
}
diff --git a/client/src/api/actions.ts b/client/src/api/actions.ts
new file mode 100644
index 00000000..003ad2bc
--- /dev/null
+++ b/client/src/api/actions.ts
@@ -0,0 +1,5 @@
+import Game from "./game";
+import { createAction } from "@reduxjs/toolkit";
+
+export const gameLoaded = createAction("game/loaded");
+export const gameUnloaded = createAction("game/unloaded");
diff --git a/client/src/api/controlPointsSlice.ts b/client/src/api/controlPointsSlice.ts
index 15cd8d96..085c9d69 100644
--- a/client/src/api/controlPointsSlice.ts
+++ b/client/src/api/controlPointsSlice.ts
@@ -1,4 +1,5 @@
import { RootState } from "../app/store";
+import { gameLoaded, gameUnloaded } from "./actions";
import { ControlPoint } from "./controlpoint";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
@@ -14,21 +15,28 @@ export const controlPointsSlice = createSlice({
name: "controlPoints",
initialState,
reducers: {
- setControlPoints: (state, action: PayloadAction) => {
- state.controlPoints = {};
- for (const cp of action.payload) {
- state.controlPoints[cp.id] = cp;
- }
- },
updateControlPoint: (state, action: PayloadAction) => {
const cp = action.payload;
state.controlPoints[cp.id] = cp;
},
},
+ extraReducers: (builder) => {
+ builder.addCase(gameLoaded, (state, action) => {
+ state.controlPoints = action.payload.control_points.reduce(
+ (acc: { [key: number]: ControlPoint }, curr) => {
+ acc[curr.id] = curr;
+ return acc;
+ },
+ {}
+ );
+ });
+ builder.addCase(gameUnloaded, (state) => {
+ state.controlPoints = {};
+ });
+ },
});
-export const { setControlPoints, updateControlPoint } =
- controlPointsSlice.actions;
+export const { updateControlPoint } = controlPointsSlice.actions;
export const selectControlPoints = (state: RootState) => state.controlPoints;
diff --git a/client/src/api/eventstream.tsx b/client/src/api/eventstream.tsx
index 04615be7..eeed7485 100644
--- a/client/src/api/eventstream.tsx
+++ b/client/src/api/eventstream.tsx
@@ -1,4 +1,5 @@
import { AppDispatch } from "../app/store";
+import { gameUnloaded } from "./actions";
import backend from "./backend";
import Combat from "./combat";
import { endCombat, newCombat, updateCombat } from "./combatSlice";
@@ -19,6 +20,7 @@ import {
updateFrontLine,
} from "./frontLinesSlice";
import FrontLine from "./frontline";
+import reloadGameState from "./gamestate";
import Tgo from "./tgo";
import { updateTgo } from "./tgosSlice";
import { LatLng } from "leaflet";
@@ -41,6 +43,8 @@ interface GameUpdateEvents {
deleted_front_lines: string[];
updated_tgos: string[];
updated_control_points: number[];
+ reset_on_map_center: LatLng | null;
+ game_unloaded: boolean;
}
export const handleStreamedEvents = (
@@ -114,4 +118,12 @@ export const handleStreamedEvents = (
dispatch(updateControlPoint(cp));
});
}
+
+ if (events.reset_on_map_center != null) {
+ reloadGameState(dispatch);
+ }
+
+ if (events.game_unloaded) {
+ dispatch(gameUnloaded());
+ }
};
diff --git a/client/src/api/flightsSlice.ts b/client/src/api/flightsSlice.ts
index ff4fd2b1..1f5a2a23 100644
--- a/client/src/api/flightsSlice.ts
+++ b/client/src/api/flightsSlice.ts
@@ -1,4 +1,5 @@
import { RootState } from "../app/store";
+import { gameLoaded, gameUnloaded } from "./actions";
import { Flight } from "./flight";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { LatLng } from "leaflet";
@@ -17,9 +18,6 @@ export const flightsSlice = createSlice({
name: "flights",
initialState,
reducers: {
- clearFlights: (state) => {
- state.flights = {};
- },
registerFlight: (state, action: PayloadAction) => {
const flight = action.payload;
if (flight.id in state.flights) {
@@ -51,10 +49,25 @@ export const flightsSlice = createSlice({
}
},
},
+ extraReducers: (builder) => {
+ builder.addCase(gameLoaded, (state, action) => {
+ state.selected = null;
+ state.flights = action.payload.flights.reduce(
+ (acc: { [key: string]: Flight }, curr) => {
+ acc[curr.id] = curr;
+ return acc;
+ },
+ {}
+ );
+ });
+ builder.addCase(gameUnloaded, (state) => {
+ state.selected = null;
+ state.flights = {};
+ });
+ },
});
export const {
- clearFlights,
registerFlight,
unregisterFlight,
updateFlight,
diff --git a/client/src/api/frontLinesSlice.ts b/client/src/api/frontLinesSlice.ts
index 41c4be9a..3b1af109 100644
--- a/client/src/api/frontLinesSlice.ts
+++ b/client/src/api/frontLinesSlice.ts
@@ -1,4 +1,5 @@
import { RootState } from "../app/store";
+import { gameLoaded, gameUnloaded } from "./actions";
import FrontLine from "./frontline";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
@@ -14,12 +15,6 @@ export const frontLinesSlice = createSlice({
name: "frontLines",
initialState,
reducers: {
- setFrontLines: (state, action: PayloadAction) => {
- state.fronts = {};
- for (const front of action.payload) {
- state.fronts[front.id] = front;
- }
- },
addFrontLine: (state, action: PayloadAction) => {
const front = action.payload;
state.fronts[front.id] = front;
@@ -32,9 +27,23 @@ export const frontLinesSlice = createSlice({
delete state.fronts[action.payload];
},
},
+ extraReducers: (builder) => {
+ builder.addCase(gameLoaded, (state, action) => {
+ state.fronts = action.payload.front_lines.reduce(
+ (acc: { [key: string]: FrontLine }, curr) => {
+ acc[curr.id] = curr;
+ return acc;
+ },
+ {}
+ );
+ });
+ builder.addCase(gameUnloaded, (state) => {
+ state.fronts = {};
+ });
+ },
});
-export const { setFrontLines, addFrontLine, updateFrontLine, deleteFrontLine } =
+export const { addFrontLine, updateFrontLine, deleteFrontLine } =
frontLinesSlice.actions;
export const selectFrontLines = (state: RootState) => state.frontLines;
diff --git a/client/src/api/game.ts b/client/src/api/game.ts
new file mode 100644
index 00000000..04ff805e
--- /dev/null
+++ b/client/src/api/game.ts
@@ -0,0 +1,15 @@
+import { ControlPoint } from "./controlpoint";
+import { Flight } from "./flight";
+import FrontLine from "./frontline";
+import SupplyRoute from "./supplyroute";
+import Tgo from "./tgo";
+import { LatLngLiteral } from "leaflet";
+
+export default interface Game {
+ control_points: ControlPoint[];
+ tgos: Tgo[];
+ supply_routes: SupplyRoute[];
+ front_lines: FrontLine[];
+ flights: Flight[];
+ map_center: LatLngLiteral;
+}
diff --git a/client/src/api/gamestate.ts b/client/src/api/gamestate.ts
new file mode 100644
index 00000000..3924bc46
--- /dev/null
+++ b/client/src/api/gamestate.ts
@@ -0,0 +1,17 @@
+import { AppDispatch } from "../app/store";
+import { gameLoaded, gameUnloaded } from "./actions";
+import backend from "./backend";
+import Game from "./game";
+
+export default function reloadGameState(dispatch: AppDispatch) {
+ backend
+ .get("/game")
+ .catch((error) => console.log(`Error fetching game state: ${error}`))
+ .then((response) => {
+ if (response == null || response.data == null) {
+ dispatch(gameUnloaded());
+ return;
+ }
+ dispatch(gameLoaded(response.data as Game));
+ });
+}
diff --git a/client/src/api/mapSlice.ts b/client/src/api/mapSlice.ts
new file mode 100644
index 00000000..9906dccb
--- /dev/null
+++ b/client/src/api/mapSlice.ts
@@ -0,0 +1,30 @@
+import { RootState } from "../app/store";
+import { gameLoaded, gameUnloaded } from "./actions";
+import { createSlice } from "@reduxjs/toolkit";
+import { LatLngLiteral } from "leaflet";
+
+interface MapState {
+ center: LatLngLiteral;
+}
+
+const initialState: MapState = {
+ center: { lat: 0, lng: 0 },
+};
+
+const mapSlice = createSlice({
+ name: "map",
+ initialState: initialState,
+ reducers: {},
+ extraReducers: (builder) => {
+ builder.addCase(gameLoaded, (state, action) => {
+ state.center = action.payload.map_center;
+ });
+ builder.addCase(gameUnloaded, (state) => {
+ state.center = { lat: 0, lng: 0 };
+ });
+ },
+});
+
+export const selectMapCenter = (state: RootState) => state.map.center;
+
+export default mapSlice.reducer;
diff --git a/client/src/api/supplyRoutesSlice.ts b/client/src/api/supplyRoutesSlice.ts
index da228987..b6fc0639 100644
--- a/client/src/api/supplyRoutesSlice.ts
+++ b/client/src/api/supplyRoutesSlice.ts
@@ -1,6 +1,7 @@
import { RootState } from "../app/store";
+import { gameLoaded, gameUnloaded } from "./actions";
import SupplyRoute from "./supplyroute";
-import { PayloadAction, createSlice } from "@reduxjs/toolkit";
+import { createSlice } from "@reduxjs/toolkit";
interface SupplyRoutesState {
routes: SupplyRoute[];
@@ -13,15 +14,17 @@ const initialState: SupplyRoutesState = {
export const supplyRoutesSlice = createSlice({
name: "supplyRoutes",
initialState,
- reducers: {
- setSupplyRoutes: (state, action: PayloadAction) => {
- state.routes = action.payload;
- },
+ reducers: {},
+ extraReducers: (builder) => {
+ builder.addCase(gameLoaded, (state, action) => {
+ state.routes = action.payload.supply_routes;
+ });
+ builder.addCase(gameUnloaded, (state) => {
+ state.routes = [];
+ });
},
});
-export const { setSupplyRoutes } = supplyRoutesSlice.actions;
-
export const selectSupplyRoutes = (state: RootState) => state.supplyRoutes;
export default supplyRoutesSlice.reducer;
diff --git a/client/src/api/tgosSlice.ts b/client/src/api/tgosSlice.ts
index fa17a184..52b838bb 100644
--- a/client/src/api/tgosSlice.ts
+++ b/client/src/api/tgosSlice.ts
@@ -1,4 +1,5 @@
import { RootState } from "../app/store";
+import { gameLoaded, gameUnloaded } from "./actions";
import { Tgo } from "./tgo";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
@@ -14,20 +15,28 @@ export const tgosSlice = createSlice({
name: "tgos",
initialState,
reducers: {
- setTgos: (state, action: PayloadAction) => {
- state.tgos = {};
- for (const tgo of action.payload) {
- state.tgos[tgo.id] = tgo;
- }
- },
updateTgo: (state, action: PayloadAction) => {
const tgo = action.payload;
state.tgos[tgo.id] = tgo;
},
},
+ extraReducers: (builder) => {
+ builder.addCase(gameLoaded, (state, action) => {
+ state.tgos = action.payload.tgos.reduce(
+ (acc: { [key: string]: Tgo }, curr) => {
+ acc[curr.id] = curr;
+ return acc;
+ },
+ {}
+ );
+ });
+ builder.addCase(gameUnloaded, (state) => {
+ state.tgos = {};
+ });
+ },
});
-export const { setTgos, updateTgo } = tgosSlice.actions;
+export const { updateTgo } = tgosSlice.actions;
export const selectTgos = (state: RootState) => state.tgos;
diff --git a/client/src/app/store.ts b/client/src/app/store.ts
index 48492b87..203c427b 100644
--- a/client/src/app/store.ts
+++ b/client/src/app/store.ts
@@ -3,6 +3,7 @@ import combatReducer from "../api/combatSlice";
import controlPointsReducer from "../api/controlPointsSlice";
import flightsReducer from "../api/flightsSlice";
import frontLinesReducer from "../api/frontLinesSlice";
+import mapReducer from "../api/mapSlice";
import supplyRoutesReducer from "../api/supplyRoutesSlice";
import tgosReducer from "../api/tgosSlice";
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
@@ -13,6 +14,7 @@ export const store = configureStore({
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
+ map: mapReducer,
supplyRoutes: supplyRoutesReducer,
tgos: tgosReducer,
[apiSlice.reducerPath]: apiSlice.reducer,
diff --git a/client/src/components/liberationmap/LiberationMap.tsx b/client/src/components/liberationmap/LiberationMap.tsx
index 47f52a54..ac6a61f3 100644
--- a/client/src/components/liberationmap/LiberationMap.tsx
+++ b/client/src/components/liberationmap/LiberationMap.tsx
@@ -1,3 +1,5 @@
+import { selectMapCenter } from "../../api/mapSlice";
+import { useAppSelector } from "../../app/hooks";
import AircraftLayer from "../aircraftlayer";
import AirDefenseRangeLayer from "../airdefenserangelayer";
import CombatLayer from "../combatlayer";
@@ -7,17 +9,23 @@ import FrontLinesLayer from "../frontlineslayer";
import SupplyRoutesLayer from "../supplyrouteslayer";
import TgosLayer from "../tgoslayer/TgosLayer";
import "./LiberationMap.css";
-import { LatLng } from "leaflet";
+import { Map } from "leaflet";
+import { useEffect, useRef } from "react";
import { BasemapLayer } from "react-esri-leaflet";
import { LayersControl, MapContainer, ScaleControl } from "react-leaflet";
-interface GameProps {
- mapCenter: LatLng;
-}
-
-export default function LiberationMap(props: GameProps) {
+export default function LiberationMap() {
+ const map = useRef