diff --git a/client/src/api/eventstream.tsx b/client/src/api/eventstream.tsx index 21e589b5..6b11b9bd 100644 --- a/client/src/api/eventstream.tsx +++ b/client/src/api/eventstream.tsx @@ -22,6 +22,7 @@ import { import FrontLine from "./frontline"; import reloadGameState from "./gamestate"; import { liberationApi } from "./liberationApi"; +import { navMeshUpdated } from "./navMeshSlice"; import Tgo from "./tgo"; import { updateTgo } from "./tgosSlice"; import { threatZonesUpdated } from "./threatZonesSlice"; @@ -72,6 +73,16 @@ export const handleStreamedEvents = ( dispatch(endCombat(id)); } + for (const blue of events.navmesh_updates) { + dispatch( + liberationApi.endpoints.getNavmesh.initiate({ forPlayer: blue }) + ).then((result) => { + if (result.data) { + dispatch(navMeshUpdated({ blue: blue, mesh: result.data })); + } + }); + } + if (events.threat_zones_updated) { dispatch(liberationApi.endpoints.getThreatZones.initiate()).then( (result) => { diff --git a/client/src/api/game.ts b/client/src/api/game.ts index 4377e2a5..764daf79 100644 --- a/client/src/api/game.ts +++ b/client/src/api/game.ts @@ -1,7 +1,7 @@ import { ControlPoint } from "./controlpoint"; import { Flight } from "./flight"; import FrontLine from "./frontline"; -import { ThreatZoneContainer } from "./liberationApi"; +import { NavMeshes, ThreatZoneContainer } from "./liberationApi"; import SupplyRoute from "./supplyroute"; import Tgo from "./tgo"; import { LatLngLiteral } from "leaflet"; @@ -13,5 +13,6 @@ export default interface Game { front_lines: FrontLine[]; flights: Flight[]; threat_zones: ThreatZoneContainer; + navmeshes: NavMeshes; map_center: LatLngLiteral | null; } diff --git a/client/src/api/liberationApi.ts b/client/src/api/liberationApi.ts index 13e3cc4d..84683a4c 100644 --- a/client/src/api/liberationApi.ts +++ b/client/src/api/liberationApi.ts @@ -280,7 +280,7 @@ export type GetThreatZonesApiResponse = /** status 200 Successful Response */ ThreatZoneContainer; export type GetThreatZonesApiArg = void; export type GetNavmeshApiResponse = - /** status 200 Successful Response */ NavMeshPoly[]; + /** status 200 Successful Response */ NavMesh; export type GetNavmeshApiArg = { forPlayer: boolean; }; @@ -414,23 +414,6 @@ export type SupplyRoute = { blue: boolean; active_transports: string[]; }; -export type Game = { - control_points: ControlPoint[]; - tgos: Tgo[]; - supply_routes: SupplyRoute[]; - front_lines: FrontLine[]; - flights: Flight[]; - map_center: LatLng; -}; -export type MapZones = { - inclusion: number[][][]; - exclusion: number[][][]; - sea: number[][][]; -}; -export type UnculledZone = { - position: LatLng; - radius: number; -}; export type ThreatZones = { full: number[][][]; aircraft: number[][][]; @@ -445,6 +428,32 @@ export type NavMeshPoly = { poly: number[][]; threatened: boolean; }; +export type NavMesh = { + polys: NavMeshPoly[]; +}; +export type NavMeshes = { + blue: NavMesh; + red: NavMesh; +}; +export type Game = { + control_points: ControlPoint[]; + tgos: Tgo[]; + supply_routes: SupplyRoute[]; + front_lines: FrontLine[]; + flights: Flight[]; + threat_zones: ThreatZoneContainer; + navmeshes: NavMeshes; + map_center: LatLng; +}; +export type MapZones = { + inclusion: number[][][]; + exclusion: number[][][]; + sea: number[][][]; +}; +export type UnculledZone = { + position: LatLng; + radius: number; +}; export const { useListControlPointsQuery, useGetControlPointByIdQuery, diff --git a/client/src/api/navMeshSlice.ts b/client/src/api/navMeshSlice.ts new file mode 100644 index 00000000..ea1f0ed0 --- /dev/null +++ b/client/src/api/navMeshSlice.ts @@ -0,0 +1,50 @@ +import { RootState } from "../app/store"; +import { gameLoaded, gameUnloaded } from "./actions"; +import { NavMesh, NavMeshPoly } from "./liberationApi"; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface NavMeshState { + blue: NavMeshPoly[]; + red: NavMeshPoly[]; +} + +const initialState: NavMeshState = { + blue: [], + red: [], +}; + +interface INavMeshUpdate { + blue: boolean; + mesh: NavMesh; +} + +const navMeshSlice = createSlice({ + name: "navmesh", + initialState: initialState, + reducers: { + updated: (state, action: PayloadAction) => { + const polys = action.payload.mesh.polys; + if (action.payload.blue) { + state.blue = polys; + } else { + state.red = polys; + } + }, + }, + extraReducers: (builder) => { + builder.addCase(gameLoaded, (state, action) => { + state.blue = action.payload.navmeshes.blue.polys; + state.red = action.payload.navmeshes.red.polys; + }); + builder.addCase(gameUnloaded, (state) => { + state.blue = []; + state.red = []; + }); + }, +}); + +export const { updated: navMeshUpdated } = navMeshSlice.actions; + +export const selectNavMeshes = (state: RootState) => state.navmeshes; + +export default navMeshSlice.reducer; diff --git a/client/src/app/store.ts b/client/src/app/store.ts index 4877849f..5357df6d 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -4,6 +4,7 @@ import controlPointsReducer from "../api/controlPointsSlice"; import flightsReducer from "../api/flightsSlice"; import frontLinesReducer from "../api/frontLinesSlice"; import mapReducer from "../api/mapSlice"; +import navMeshReducer from "../api/navMeshSlice"; import supplyRoutesReducer from "../api/supplyRoutesSlice"; import tgosReducer from "../api/tgosSlice"; import threatZonesReducer from "../api/threatZonesSlice"; @@ -16,6 +17,7 @@ export const store = configureStore({ flights: flightsReducer, frontLines: frontLinesReducer, map: mapReducer, + navmeshes: navMeshReducer, supplyRoutes: supplyRoutesReducer, tgos: tgosReducer, threatZones: threatZonesReducer, diff --git a/client/src/components/liberationmap/LiberationMap.tsx b/client/src/components/liberationmap/LiberationMap.tsx index a6e067b1..9a97fadc 100644 --- a/client/src/components/liberationmap/LiberationMap.tsx +++ b/client/src/components/liberationmap/LiberationMap.tsx @@ -6,6 +6,7 @@ import CombatLayer from "../combatlayer"; import ControlPointsLayer from "../controlpointslayer"; import FlightPlansLayer from "../flightplanslayer"; import FrontLinesLayer from "../frontlineslayer"; +import NavMeshLayer from "../navmesh/NavMeshLayer"; import SupplyRoutesLayer from "../supplyrouteslayer"; import TgosLayer from "../tgoslayer/TgosLayer"; import { CoalitionThreatZones } from "../threatzones"; @@ -87,6 +88,12 @@ export default function LiberationMap() { + + + + + + ); diff --git a/client/src/components/navmesh/NavMeshLayer.tsx b/client/src/components/navmesh/NavMeshLayer.tsx new file mode 100644 index 00000000..c8c65466 --- /dev/null +++ b/client/src/components/navmesh/NavMeshLayer.tsx @@ -0,0 +1,33 @@ +import { selectNavMeshes } from "../../api/navMeshSlice"; +import { useAppSelector } from "../../app/hooks"; +import { LatLng } from "leaflet"; +import { LayerGroup, Polygon } from "react-leaflet"; + +interface NavMeshLayerProps { + blue: boolean; +} + +export default function NavMeshLayer(props: NavMeshLayerProps) { + const meshes = useAppSelector(selectNavMeshes); + const mesh = props.blue ? meshes.blue : meshes.red; + return ( + + {mesh.map((zone, idx) => { + const positions = zone.poly.map(([lat, lng]) => new LatLng(lat, lng)); + return ( + + ); + })} + + ); +} diff --git a/client/src/components/navmesh/index.ts b/client/src/components/navmesh/index.ts new file mode 100644 index 00000000..f867c680 --- /dev/null +++ b/client/src/components/navmesh/index.ts @@ -0,0 +1 @@ +export { default } from "./NavMeshLayer"; diff --git a/game/server/game/models.py b/game/server/game/models.py index 7e933064..3ac7ad36 100644 --- a/game/server/game/models.py +++ b/game/server/game/models.py @@ -9,6 +9,7 @@ from game.server.flights.models import FlightJs from game.server.frontlines.models import FrontLineJs from game.server.leaflet import LeafletPoint from game.server.mapzones.models import ThreatZoneContainerJs +from game.server.navmesh.models import NavMeshesJs from game.server.supplyroutes.models import SupplyRouteJs from game.server.tgos.models import TgoJs @@ -23,6 +24,7 @@ class GameJs(BaseModel): front_lines: list[FrontLineJs] flights: list[FlightJs] threat_zones: ThreatZoneContainerJs + navmeshes: NavMeshesJs map_center: LeafletPoint class Config: @@ -37,5 +39,6 @@ class GameJs(BaseModel): front_lines=FrontLineJs.all_in_game(game), flights=FlightJs.all_in_game(game, with_waypoints=True), threat_zones=ThreatZoneContainerJs.for_game(game), + navmeshes=NavMeshesJs.from_game(game), map_center=game.theater.terrain.map_view_default.position.latlng(), ) diff --git a/game/server/navmesh/models.py b/game/server/navmesh/models.py index 672f153e..69c97275 100644 --- a/game/server/navmesh/models.py +++ b/game/server/navmesh/models.py @@ -1,8 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from pydantic import BaseModel -from game.server.leaflet import LeafletPoly +from game.server.leaflet import LeafletPoly, ShapelyUtil + +if TYPE_CHECKING: + from game import Game + from game.navmesh import NavMesh class NavMeshPolyJs(BaseModel): @@ -11,3 +17,37 @@ class NavMeshPolyJs(BaseModel): class Config: title = "NavMeshPoly" + + +class NavMeshJs(BaseModel): + polys: list[NavMeshPolyJs] + + class Config: + title = "NavMesh" + + @staticmethod + def from_navmesh(navmesh: NavMesh, game: Game) -> NavMeshJs: + return NavMeshJs( + polys=[ + NavMeshPolyJs( + poly=ShapelyUtil.poly_to_leaflet(p.poly, game.theater), + threatened=p.threatened, + ) + for p in navmesh.polys + ] + ) + + +class NavMeshesJs(BaseModel): + blue: NavMeshJs + red: NavMeshJs + + class Config: + title = "NavMeshes" + + @staticmethod + def from_game(game: Game) -> NavMeshesJs: + return NavMeshesJs( + blue=NavMeshJs.from_navmesh(game.blue.nav_mesh, game), + red=NavMeshJs.from_navmesh(game.red.nav_mesh, game), + ) diff --git a/game/server/navmesh/routes.py b/game/server/navmesh/routes.py index b7858612..01b390e1 100644 --- a/game/server/navmesh/routes.py +++ b/game/server/navmesh/routes.py @@ -2,21 +2,12 @@ from fastapi import APIRouter, Depends from game import Game from game.server import GameContext -from .models import NavMeshPolyJs -from ..leaflet import ShapelyUtil +from .models import NavMeshJs router: APIRouter = APIRouter(prefix="/navmesh") -@router.get("/", operation_id="get_navmesh", response_model=list[NavMeshPolyJs]) -def get( - for_player: bool, game: Game = Depends(GameContext.require) -) -> list[NavMeshPolyJs]: +@router.get("/", operation_id="get_navmesh", response_model=NavMeshJs) +def get(for_player: bool, game: Game = Depends(GameContext.require)) -> NavMeshJs: mesh = game.coalition_for(for_player).nav_mesh - return [ - NavMeshPolyJs( - poly=ShapelyUtil.poly_to_leaflet(p.poly, game.theater), - threatened=p.threatened, - ) - for p in mesh.polys - ] + return NavMeshJs.from_navmesh(mesh, game)