diff --git a/client/package-lock.json b/client/package-lock.json
index 5d2a7404..a43b9841 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -32,6 +32,7 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/leaflet": "^1.7.9",
+ "@types/websocket": "^1.0.5",
"electron": "^17.1.0",
"electron-is-dev": "^2.0.0",
"react-scripts": "5.0.0",
@@ -4219,6 +4220,15 @@
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
"dev": true
},
+ "node_modules/@types/websocket": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz",
+ "integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/ws": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",
@@ -21771,6 +21781,15 @@
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
"dev": true
},
+ "@types/websocket": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.5.tgz",
+ "integrity": "sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/ws": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.1.tgz",
diff --git a/client/package.json b/client/package.json
index 7ef564a9..4b2ad76d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -50,6 +50,7 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/leaflet": "^1.7.9",
+ "@types/websocket": "^1.0.5",
"electron": "^17.1.0",
"electron-is-dev": "^2.0.0",
"react-scripts": "5.0.0",
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 54bf3f0c..365062c3 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -2,12 +2,14 @@ import "./App.css";
import { LatLng } from "leaflet";
import LiberationMap from "./components/liberationmap";
-import useInitialGameState from "./api/useInitialGameState";
+import useEventStream from "./hooks/useEventSteam";
+import useInitialGameState from "./hooks/useInitialGameState";
function App() {
const mapCenter: LatLng = new LatLng(25.58, 54.9);
useInitialGameState();
+ useEventStream();
return (
diff --git a/client/src/api/backend.ts b/client/src/api/backend.ts
index 7835e3b1..ca2e6133 100644
--- a/client/src/api/backend.ts
+++ b/client/src/api/backend.ts
@@ -4,4 +4,6 @@ export const backend = axios.create({
baseURL: "http://[::1]:5000/",
});
+export const WEBSOCKET_URL = "ws://[::1]:5000/eventstream";
+
export default backend;
diff --git a/client/src/api/eventstream.tsx b/client/src/api/eventstream.tsx
new file mode 100644
index 00000000..41e815a9
--- /dev/null
+++ b/client/src/api/eventstream.tsx
@@ -0,0 +1,36 @@
+import { deselectFlight, selectFlight } from "./flightsSlice";
+
+import { AppDispatch } from "../app/store";
+import { Flight } from "./flight";
+import { LatLng } from "leaflet";
+
+// Placeholder. We don't use this yet. This is just here so we can flesh out the
+// update events model.
+interface FrozenCombat {}
+
+interface GameUpdateEvents {
+ updated_flight_positions: { [id: string]: LatLng };
+ new_combats: FrozenCombat[];
+ updated_combats: FrozenCombat[];
+ ended_combats: string[];
+ navmesh_updates: boolean[];
+ unculled_zones_updated: boolean;
+ threat_zones_updated: boolean;
+ new_flights: Flight[];
+ updated_flights: string[];
+ deleted_flights: string[];
+ selected_flight: string | null;
+ deselected_flight: boolean;
+}
+
+export const handleStreamedEvents = (
+ dispatch: AppDispatch,
+ events: GameUpdateEvents
+) => {
+ if (events.deselected_flight) {
+ dispatch(deselectFlight());
+ }
+ if (events.selected_flight != null) {
+ dispatch(selectFlight(events.selected_flight));
+ }
+};
diff --git a/client/src/api/flightsSlice.ts b/client/src/api/flightsSlice.ts
index ff8fbea8..3897c3cb 100644
--- a/client/src/api/flightsSlice.ts
+++ b/client/src/api/flightsSlice.ts
@@ -6,11 +6,13 @@ import { RootState } from "../app/store";
interface FlightsState {
blue: { [id: string]: Flight };
red: { [id: string]: Flight };
+ selected: Flight | null;
}
const initialState: FlightsState = {
blue: {},
red: {},
+ selected: null,
};
export const flightsSlice = createSlice({
@@ -42,11 +44,23 @@ export const flightsSlice = createSlice({
);
}
},
+ deselectFlight: (state) => {
+ state.selected = null;
+ },
+ selectFlight: (state, action: PayloadAction
) => {
+ const id = action.payload;
+ state.selected = state.blue[id];
+ },
},
});
-export const { clearFlights, registerFlight, unregisterFlight } =
- flightsSlice.actions;
+export const {
+ clearFlights,
+ registerFlight,
+ unregisterFlight,
+ deselectFlight,
+ selectFlight,
+} = flightsSlice.actions;
export const selectFlights = (state: RootState) => state.flights;
diff --git a/client/src/components/flightplan/FlightPlan.tsx b/client/src/components/flightplan/FlightPlan.tsx
index 0674c70e..7ef7375c 100644
--- a/client/src/components/flightplan/FlightPlan.tsx
+++ b/client/src/components/flightplan/FlightPlan.tsx
@@ -3,14 +3,25 @@ import { Polyline } from "react-leaflet";
const BLUE_PATH = "#0084ff";
const RED_PATH = "#c85050";
+const SELECTED_PATH = "#ffff00";
interface FlightPlanProps {
flight: Flight;
selected: boolean;
}
+const pathColor = (props: FlightPlanProps) => {
+ if (props.selected) {
+ return SELECTED_PATH;
+ } else if (props.flight.blue) {
+ return BLUE_PATH;
+ } else {
+ return RED_PATH;
+ }
+};
+
function FlightPlanPath(props: FlightPlanProps) {
- const color = props.flight.blue ? BLUE_PATH : RED_PATH;
+ const color = pathColor(props);
const waypoints = props.flight.waypoints;
if (waypoints == null) {
return <>>;
diff --git a/client/src/components/flightplanslayer/FlightPlansLayer.tsx b/client/src/components/flightplanslayer/FlightPlansLayer.tsx
index e094064c..253d7454 100644
--- a/client/src/components/flightplanslayer/FlightPlansLayer.tsx
+++ b/client/src/components/flightplanslayer/FlightPlansLayer.tsx
@@ -1,3 +1,4 @@
+import { Flight } from "../../api/flight";
import FlightPlan from "../flightplan";
import { LayerGroup } from "react-leaflet";
import { selectFlights } from "../../api/flightsSlice";
@@ -10,11 +11,29 @@ interface FlightPlansLayerProps {
export default function FlightPlansLayer(props: FlightPlansLayerProps) {
const atos = useAppSelector(selectFlights);
const flights = props.blue ? atos.blue : atos.red;
+ const isNotSelected = (flight: Flight) => {
+ if (atos.selected == null) {
+ return true;
+ }
+ return atos.selected.id !== flight.id;
+ };
+
+ const selectedFlight = atos.selected ? (
+
+ ) : (
+ <>>
+ );
+
return (
- {Object.values(flights).map((flight) => {
- return ;
- })}
+ {Object.values(flights)
+ .filter(isNotSelected)
+ .map((flight) => {
+ return (
+
+ );
+ })}
+ {selectedFlight}
);
}
diff --git a/client/src/components/socketprovider/socketprovider.tsx b/client/src/components/socketprovider/socketprovider.tsx
new file mode 100644
index 00000000..9b5e48b3
--- /dev/null
+++ b/client/src/components/socketprovider/socketprovider.tsx
@@ -0,0 +1,36 @@
+import { ReactChild, createContext, useEffect, useState } from "react";
+
+import { WEBSOCKET_URL } from "../../api/backend";
+
+const socket = new WebSocket(WEBSOCKET_URL);
+
+export const SocketContext = createContext(socket);
+
+interface SocketProviderProps {
+ children: ReactChild;
+}
+
+export const SocketProvider = (props: SocketProviderProps) => {
+ const [ws, setWs] = useState(socket);
+ useEffect(() => {
+ const onClose = () => {
+ setWs(new WebSocket(WEBSOCKET_URL));
+ };
+
+ const onError = (error: Event) => {
+ console.log(`Websocket error: ${error}`);
+ };
+
+ ws.addEventListener("close", onClose);
+ ws.addEventListener("error", onError);
+
+ return () => {
+ ws.removeEventListener("close", onClose);
+ ws.removeEventListener("error", onError);
+ };
+ });
+
+ return (
+ {props.children}
+ );
+};
diff --git a/client/src/hooks/useEventSteam.ts b/client/src/hooks/useEventSteam.ts
new file mode 100644
index 00000000..1545d2e8
--- /dev/null
+++ b/client/src/hooks/useEventSteam.ts
@@ -0,0 +1,26 @@
+import { useCallback, useEffect } from "react";
+
+import { handleStreamedEvents } from "../api/eventstream";
+import { useAppDispatch } from "../app/hooks";
+import { useSocket } from "./useSocket";
+
+export const useEventStream = () => {
+ const ws = useSocket();
+ const dispatch = useAppDispatch();
+
+ const onMessage = useCallback(
+ (message) => {
+ handleStreamedEvents(dispatch, JSON.parse(message.data));
+ },
+ [dispatch]
+ );
+
+ useEffect(() => {
+ ws.addEventListener("message", onMessage);
+ return () => {
+ ws.removeEventListener("message", onMessage);
+ };
+ });
+};
+
+export default useEventStream;
diff --git a/client/src/api/useInitialGameState.tsx b/client/src/hooks/useInitialGameState.tsx
similarity index 81%
rename from client/src/api/useInitialGameState.tsx
rename to client/src/hooks/useInitialGameState.tsx
index dac752f2..9e7f827d 100644
--- a/client/src/api/useInitialGameState.tsx
+++ b/client/src/hooks/useInitialGameState.tsx
@@ -1,8 +1,8 @@
-import { ControlPoint } from "./controlpoint";
-import { Flight } from "./flight";
-import backend from "./backend";
-import { registerFlight } from "./flightsSlice";
-import { setControlPoints } from "./controlPointsSlice";
+import { ControlPoint } from "../api/controlpoint";
+import { Flight } from "../api/flight";
+import backend from "../api/backend";
+import { registerFlight } from "../api/flightsSlice";
+import { setControlPoints } from "../api/controlPointsSlice";
import { useAppDispatch } from "../app/hooks";
import { useEffect } from "react";
diff --git a/client/src/hooks/useSocket.ts b/client/src/hooks/useSocket.ts
new file mode 100644
index 00000000..06176bb3
--- /dev/null
+++ b/client/src/hooks/useSocket.ts
@@ -0,0 +1,8 @@
+import { SocketContext } from "../components/socketprovider/socketprovider";
+import { useContext } from "react";
+
+export const useSocket = () => {
+ const socket = useContext(SocketContext);
+
+ return socket;
+};
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 3468b9c5..5e231cac 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -6,12 +6,15 @@ import App from "./App";
import { Provider } from "react-redux";
import React from "react";
import ReactDOM from "react-dom";
+import { SocketProvider } from "./components/socketprovider/socketprovider";
import { store } from "./app/store";
ReactDOM.render(
-
+
+
+
,
document.getElementById("root")