Handle map reset when the game is loaded/unloaded.

https://github.com/dcs-liberation/dcs_liberation/issues/2039

Partial fix for
https://github.com/dcs-liberation/dcs_liberation/issues/2045 (now works
in the new map, old one not fixed yet).
This commit is contained in:
Dan Albert
2022-03-05 18:02:46 -08:00
parent 995e28cb32
commit 73fcfcec7b
37 changed files with 403 additions and 178 deletions

View File

@@ -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 (
<div className="App">
<LiberationMap mapCenter={mapCenter} />
<LiberationMap />
</div>
);
}

View File

@@ -0,0 +1,5 @@
import Game from "./game";
import { createAction } from "@reduxjs/toolkit";
export const gameLoaded = createAction<Game>("game/loaded");
export const gameUnloaded = createAction("game/unloaded");

View File

@@ -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<ControlPoint[]>) => {
state.controlPoints = {};
for (const cp of action.payload) {
state.controlPoints[cp.id] = cp;
}
},
updateControlPoint: (state, action: PayloadAction<ControlPoint>) => {
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;

View File

@@ -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());
}
};

View File

@@ -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<Flight>) => {
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,

View File

@@ -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<FrontLine[]>) => {
state.fronts = {};
for (const front of action.payload) {
state.fronts[front.id] = front;
}
},
addFrontLine: (state, action: PayloadAction<FrontLine>) => {
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;

15
client/src/api/game.ts Normal file
View File

@@ -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;
}

View File

@@ -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));
});
}

View File

@@ -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;

View File

@@ -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<SupplyRoute[]>) => {
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;

View File

@@ -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<Tgo[]>) => {
state.tgos = {};
for (const tgo of action.payload) {
state.tgos[tgo.id] = tgo;
}
},
updateTgo: (state, action: PayloadAction<Tgo>) => {
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;

View File

@@ -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,

View File

@@ -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<Map>();
const mapCenter = useAppSelector(selectMapCenter);
useEffect(() => {
map.current?.setView(mapCenter, 8, { animate: true, duration: 1 });
});
return (
<MapContainer zoom={8} center={props.mapCenter} zoomControl={false}>
<MapContainer
zoom={8}
zoomControl={false}
whenCreated={(mapInstance) => (map.current = mapInstance)}
>
<ScaleControl />
<LayersControl collapsed={false}>
<LayersControl.BaseLayer name="Imagery Clarity" checked>

View File

@@ -1,14 +1,4 @@
import backend from "../api/backend";
import { setControlPoints } from "../api/controlPointsSlice";
import { ControlPoint } from "../api/controlpoint";
import { Flight } from "../api/flight";
import { registerFlight } from "../api/flightsSlice";
import { setFrontLines } from "../api/frontLinesSlice";
import FrontLine from "../api/frontline";
import { setSupplyRoutes } from "../api/supplyRoutesSlice";
import SupplyRoute from "../api/supplyroute";
import Tgo from "../api/tgo";
import { setTgos } from "../api/tgosSlice";
import reloadGameState from "../api/gamestate";
import { useAppDispatch } from "../app/hooks";
import { useEffect } from "react";
@@ -19,48 +9,7 @@ import { useEffect } from "react";
export const useInitialGameState = () => {
const dispatch = useAppDispatch();
useEffect(() => {
backend
.get("/control-points")
.catch((error) => console.log(`Error fetching control points: ${error}`))
.then((response) => {
if (response != null) {
dispatch(setControlPoints(response.data as ControlPoint[]));
}
});
backend
.get("/tgos")
.catch((error) => console.log(`Error fetching TGOs: ${error}`))
.then((response) => {
if (response != null) {
dispatch(setTgos(response.data as Tgo[]));
}
});
backend
.get("/supply-routes")
.catch((error) => console.log(`Error fetching supply routes: ${error}`))
.then((response) => {
if (response != null) {
dispatch(setSupplyRoutes(response.data as SupplyRoute[]));
}
});
backend
.get("/front-lines")
.catch((error) => console.log(`Error fetching front-lines: ${error}`))
.then((response) => {
if (response != null) {
dispatch(setFrontLines(response.data as FrontLine[]));
}
});
backend
.get("/flights?with_waypoints=true")
.catch((error) => console.log(`Error fetching flights: ${error}`))
.then((response) => {
if (response != null) {
for (const flight of response.data) {
dispatch(registerFlight(flight as Flight));
}
}
});
reloadGameState(dispatch);
});
};