mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Move NavMesh out of MapModel.
This commit is contained in:
parent
1a9930b93a
commit
0e6a303c17
@ -21,10 +21,11 @@ from game.threatzones import ThreatZones
|
||||
from game.transfers import PendingTransfers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.campaignloader import CampaignAirWingConfig
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.factions.faction import Faction
|
||||
from .campaignloader import CampaignAirWingConfig
|
||||
from .data.doctrine import Doctrine
|
||||
from .factions.faction import Faction
|
||||
from .game import Game
|
||||
from .sim import GameUpdateEvents
|
||||
|
||||
|
||||
class Coalition:
|
||||
@ -121,10 +122,11 @@ class Coalition:
|
||||
def compute_threat_zones(self) -> None:
|
||||
self._threat_zone = ThreatZones.for_faction(self.game, self.player)
|
||||
|
||||
def compute_nav_meshes(self) -> None:
|
||||
def compute_nav_meshes(self, events: GameUpdateEvents) -> None:
|
||||
self._navmesh = NavMesh.from_threat_zones(
|
||||
self.opponent.threat_zone, self.game.theater
|
||||
)
|
||||
events.update_navmesh(self.player)
|
||||
|
||||
def update_transit_network(self) -> None:
|
||||
self.transit_network = TransitNetworkBuilder(
|
||||
|
||||
39
game/game.py
39
game/game.py
@ -15,11 +15,11 @@ from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
from faker import Faker
|
||||
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from game.ground_forces.ai_ground_planner import GroundPlanner
|
||||
from game.models.game_stats import GameStats
|
||||
from game.plugins import LuaPluginManager
|
||||
from game.utils import Distance
|
||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||
from . import naming, persistency
|
||||
from .ato.flighttype import FlightType
|
||||
from .campaignloader import CampaignAirWingConfig
|
||||
@ -40,10 +40,11 @@ from .weather import Conditions, TimeOfDay
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .ato.airtaaskingorder import AirTaskingOrder
|
||||
from .factions.faction import Faction
|
||||
from .navmesh import NavMesh
|
||||
from .sim import GameUpdateEvents
|
||||
from .squadrons import AirWing
|
||||
from .threatzones import ThreatZones
|
||||
from .factions.faction import Faction
|
||||
|
||||
COMMISION_UNIT_VARIETY = 4
|
||||
COMMISION_LIMITS_SCALE = 1.5
|
||||
@ -203,6 +204,8 @@ class Game:
|
||||
self.coalition_for(player).adjust_budget(amount)
|
||||
|
||||
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||
from .sim import GameUpdateEvents
|
||||
|
||||
if not hasattr(self, "name_generator"):
|
||||
self.name_generator = naming.namegen
|
||||
# Hack: Replace the global name generator state with the state from the save
|
||||
@ -215,7 +218,9 @@ class Game:
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_unculled_zones()
|
||||
if not game_still_initializing:
|
||||
self.compute_threat_zones()
|
||||
# We don't need to push events that happen during load. The UI will fully
|
||||
# reset when we're done.
|
||||
self.compute_threat_zones(GameUpdateEvents())
|
||||
|
||||
def finish_turn(self, skipped: bool = False) -> None:
|
||||
"""Finalizes the current turn and advances to the next turn.
|
||||
@ -266,9 +271,13 @@ class Game:
|
||||
|
||||
def begin_turn_0(self) -> None:
|
||||
"""Initialization for the first turn of the game."""
|
||||
from .sim import GameUpdateEvents
|
||||
|
||||
self.blue.preinit_turn_0()
|
||||
self.red.preinit_turn_0()
|
||||
self.initialize_turn()
|
||||
# We don't need to actually stream events for turn zero because we haven't given
|
||||
# *any* state to the UI yet, so it will need to do a full draw once we do.
|
||||
self.initialize_turn(GameUpdateEvents())
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
"""Ends the current turn and initializes the new turn.
|
||||
@ -278,11 +287,18 @@ class Game:
|
||||
Args:
|
||||
no_action: True if the turn was skipped.
|
||||
"""
|
||||
from .server import EventStream
|
||||
from .sim import GameUpdateEvents
|
||||
|
||||
logging.info("Pass turn")
|
||||
with logged_duration("Turn finalization"):
|
||||
self.finish_turn(no_action)
|
||||
|
||||
events = GameUpdateEvents()
|
||||
with logged_duration("Turn initialization"):
|
||||
self.initialize_turn()
|
||||
self.initialize_turn(events)
|
||||
|
||||
EventStream.put_nowait(events)
|
||||
|
||||
# Autosave progress
|
||||
persistency.autosave(self)
|
||||
@ -307,7 +323,9 @@ class Game:
|
||||
self.blue.bullseye = Bullseye(enemy_cp.position)
|
||||
self.red.bullseye = Bullseye(player_cp.position)
|
||||
|
||||
def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None:
|
||||
def initialize_turn(
|
||||
self, events: GameUpdateEvents, for_red: bool = True, for_blue: bool = True
|
||||
) -> None:
|
||||
"""Performs turn initialization for the specified players.
|
||||
|
||||
Turn initialization performs all of the beginning-of-turn actions. *End-of-turn*
|
||||
@ -338,6 +356,7 @@ class Game:
|
||||
impactful but also likely to be early, so they also cause a blue replan.
|
||||
|
||||
Args:
|
||||
events: Game update event container for turn initialization.
|
||||
for_red: True if opfor should be re-initialized.
|
||||
for_blue: True if the player coalition should be re-initialized.
|
||||
"""
|
||||
@ -353,7 +372,7 @@ class Game:
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
with logged_duration("Threat zone computation"):
|
||||
self.compute_threat_zones()
|
||||
self.compute_threat_zones(events)
|
||||
|
||||
# Plan Coalition specific turn
|
||||
if for_blue:
|
||||
@ -401,11 +420,11 @@ class Game:
|
||||
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
|
||||
return TransitNetworkBuilder(self.theater, player).build()
|
||||
|
||||
def compute_threat_zones(self) -> None:
|
||||
def compute_threat_zones(self, events: GameUpdateEvents) -> None:
|
||||
self.blue.compute_threat_zones()
|
||||
self.red.compute_threat_zones()
|
||||
self.blue.compute_nav_meshes()
|
||||
self.red.compute_nav_meshes()
|
||||
self.blue.compute_nav_meshes(events)
|
||||
self.red.compute_nav_meshes(events)
|
||||
|
||||
def threat_zone_for(self, player: bool) -> ThreatZones:
|
||||
return self.coalition_for(player).threat_zone
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
from fastapi import Depends, FastAPI
|
||||
|
||||
from . import debuggeometries, eventstream, flights, waypoints
|
||||
from . import debuggeometries, eventstream, flights, navmesh, waypoints
|
||||
from .security import ApiKeyManager
|
||||
|
||||
app = FastAPI(dependencies=[Depends(ApiKeyManager.verify)])
|
||||
app.include_router(debuggeometries.router)
|
||||
app.include_router(eventstream.router)
|
||||
app.include_router(flights.router)
|
||||
app.include_router(navmesh.router)
|
||||
app.include_router(waypoints.router)
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
from game import Game
|
||||
from qt_ui.models import GameModel
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from qt_ui.models import GameModel
|
||||
|
||||
|
||||
class GameContext:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from asyncio import Queue
|
||||
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim import GameUpdateEvents
|
||||
|
||||
|
||||
class EventStream:
|
||||
|
||||
@ -10,13 +10,14 @@ from game.server.leaflet import LeafletLatLon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim import GameUpdateEvents
|
||||
|
||||
|
||||
class GameUpdateEventsJs(BaseModel):
|
||||
updated_flights: dict[UUID, LeafletLatLon]
|
||||
new_combats: list[FrozenCombatJs] = []
|
||||
updated_combats: list[FrozenCombatJs] = []
|
||||
navmesh_updates: set[bool] = set()
|
||||
|
||||
@classmethod
|
||||
def from_events(cls, events: GameUpdateEvents, game: Game) -> GameUpdateEventsJs:
|
||||
@ -31,4 +32,5 @@ class GameUpdateEventsJs(BaseModel):
|
||||
FrozenCombatJs.for_combat(c, game.theater)
|
||||
for c in events.updated_combats
|
||||
],
|
||||
navmesh_updates=events.navmesh_updates,
|
||||
)
|
||||
|
||||
1
game/server/navmesh/__init__.py
Normal file
1
game/server/navmesh/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .routes import router
|
||||
10
game/server/navmesh/models.py
Normal file
10
game/server/navmesh/models.py
Normal file
@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from game.server.leaflet import LeafletPoly
|
||||
|
||||
|
||||
class NavMeshPolyJs(BaseModel):
|
||||
poly: LeafletPoly
|
||||
threatened: bool
|
||||
20
game/server/navmesh/routes.py
Normal file
20
game/server/navmesh/routes.py
Normal file
@ -0,0 +1,20 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from game import Game
|
||||
from game.server import GameContext
|
||||
from .models import NavMeshPolyJs
|
||||
from ..leaflet import ShapelyUtil
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/navmesh")
|
||||
|
||||
|
||||
@router.get("/", response_model=list[NavMeshPolyJs])
|
||||
def get(for_player: bool, game: Game = Depends(GameContext.get)) -> list[NavMeshPolyJs]:
|
||||
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
|
||||
]
|
||||
@ -1 +1,2 @@
|
||||
from .gameupdateevents import GameUpdateEvents
|
||||
from .missionsimulation import MissionSimulation
|
||||
|
||||
@ -15,6 +15,7 @@ class GameUpdateEvents:
|
||||
self.new_combats: list[FrozenCombat] = []
|
||||
self.updated_combats: list[FrozenCombat] = []
|
||||
self.updated_flights: list[tuple[Flight, Point]] = []
|
||||
self.navmesh_updates: set[bool] = set()
|
||||
|
||||
@property
|
||||
def empty(self) -> bool:
|
||||
@ -24,6 +25,7 @@ class GameUpdateEvents:
|
||||
self.new_combats,
|
||||
self.updated_combats,
|
||||
self.updated_flights,
|
||||
self.navmesh_updates,
|
||||
]
|
||||
)
|
||||
|
||||
@ -38,3 +40,6 @@ class GameUpdateEvents:
|
||||
|
||||
def update_flight(self, flight: Flight, new_position: Point) -> None:
|
||||
self.updated_flights.append((flight, new_position))
|
||||
|
||||
def update_navmesh(self, player: bool) -> None:
|
||||
self.navmesh_updates.add(player)
|
||||
|
||||
@ -21,7 +21,6 @@ from .flightjs import FlightJs
|
||||
from .frontlinejs import FrontLineJs
|
||||
from .groundobjectjs import GroundObjectJs
|
||||
from .mapzonesjs import MapZonesJs
|
||||
from .navmeshjs import NavMeshJs
|
||||
from .supplyroutejs import SupplyRouteJs
|
||||
from .threatzonecontainerjs import ThreatZoneContainerJs
|
||||
from .threatzonesjs import ThreatZonesJs
|
||||
@ -55,7 +54,6 @@ class MapModel(QObject):
|
||||
flightsChanged = Signal()
|
||||
frontLinesChanged = Signal()
|
||||
threatZonesChanged = Signal()
|
||||
navmeshesChanged = Signal()
|
||||
mapZonesChanged = Signal()
|
||||
unculledZonesChanged = Signal()
|
||||
selectedFlightChanged = Signal(str)
|
||||
@ -72,7 +70,6 @@ class MapModel(QObject):
|
||||
self._threat_zones = ThreatZoneContainerJs(
|
||||
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||
)
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||
@ -102,7 +99,6 @@ class MapModel(QObject):
|
||||
self._threat_zones = ThreatZoneContainerJs(
|
||||
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||
)
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self.cleared.emit()
|
||||
@ -168,7 +164,6 @@ class MapModel(QObject):
|
||||
self.reset_atos()
|
||||
self.reset_front_lines()
|
||||
self.reset_threat_zones()
|
||||
self.reset_navmeshes()
|
||||
self.reset_map_zones()
|
||||
self.reset_unculled_zones()
|
||||
|
||||
@ -302,20 +297,12 @@ class MapModel(QObject):
|
||||
def threatZones(self) -> ThreatZoneContainerJs:
|
||||
return self._threat_zones
|
||||
|
||||
def reset_navmeshes(self) -> None:
|
||||
self._navmeshes = NavMeshJs.from_game(self.game)
|
||||
self.navmeshesChanged.emit()
|
||||
|
||||
@Property(NavMeshJs, notify=navmeshesChanged)
|
||||
def navmeshes(self) -> NavMeshJs:
|
||||
return self._navmeshes
|
||||
|
||||
def reset_map_zones(self) -> None:
|
||||
self._map_zones = MapZonesJs.from_game(self.game)
|
||||
self.mapZonesChanged.emit()
|
||||
|
||||
@Property(MapZonesJs, notify=mapZonesChanged)
|
||||
def mapZones(self) -> NavMeshJs:
|
||||
def mapZones(self) -> MapZonesJs:
|
||||
return self._map_zones
|
||||
|
||||
def on_package_change(self) -> None:
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal
|
||||
|
||||
from game import Game
|
||||
from game.navmesh import NavMesh
|
||||
from game.server.leaflet import LeafletPoly
|
||||
from game.theater import ConflictTheater
|
||||
from .navmeshpolyjs import NavMeshPolyJs
|
||||
|
||||
|
||||
class NavMeshJs(QObject):
|
||||
blueChanged = Signal()
|
||||
redChanged = Signal()
|
||||
|
||||
def __init__(self, blue: list[NavMeshPolyJs], red: list[NavMeshPolyJs]) -> None:
|
||||
super().__init__()
|
||||
self._blue = blue
|
||||
self._red = red
|
||||
# TODO: Boundary markers.
|
||||
# TODO: Numbering.
|
||||
# TODO: Localization debugging.
|
||||
|
||||
@Property(list, notify=blueChanged)
|
||||
def blue(self) -> list[LeafletPoly]:
|
||||
return self._blue
|
||||
|
||||
@Property(list, notify=redChanged)
|
||||
def red(self) -> list[LeafletPoly]:
|
||||
return self._red
|
||||
|
||||
@staticmethod
|
||||
def to_polys(navmesh: NavMesh, theater: ConflictTheater) -> list[NavMeshPolyJs]:
|
||||
polys = []
|
||||
for poly in navmesh.polys:
|
||||
polys.append(NavMeshPolyJs.from_navmesh(poly, theater))
|
||||
return polys
|
||||
|
||||
@classmethod
|
||||
def from_game(cls, game: Game) -> NavMeshJs:
|
||||
return NavMeshJs(
|
||||
cls.to_polys(game.blue.nav_mesh, game.theater),
|
||||
cls.to_polys(game.red.nav_mesh, game.theater),
|
||||
)
|
||||
@ -1,31 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal
|
||||
|
||||
from game.navmesh import NavMeshPoly
|
||||
from game.server.leaflet import LeafletPoly, ShapelyUtil
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
class NavMeshPolyJs(QObject):
|
||||
polyChanged = Signal()
|
||||
threatenedChanged = Signal()
|
||||
|
||||
def __init__(self, poly: LeafletPoly, threatened: bool) -> None:
|
||||
super().__init__()
|
||||
self._poly = poly
|
||||
self._threatened = threatened
|
||||
|
||||
@Property(list, notify=polyChanged)
|
||||
def poly(self) -> LeafletPoly:
|
||||
return self._poly
|
||||
|
||||
@Property(bool, notify=threatenedChanged)
|
||||
def threatened(self) -> bool:
|
||||
return self._threatened
|
||||
|
||||
@classmethod
|
||||
def from_navmesh(cls, poly: NavMeshPoly, theater: ConflictTheater) -> NavMeshPolyJs:
|
||||
return NavMeshPolyJs(
|
||||
ShapelyUtil.poly_to_leaflet(poly.poly, theater), poly.threatened
|
||||
)
|
||||
@ -384,7 +384,6 @@ new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||
game.frontLinesChanged.connect(drawFrontLines);
|
||||
game.flightsChanged.connect(drawAircraft);
|
||||
game.threatZonesChanged.connect(drawThreatZones);
|
||||
game.navmeshesChanged.connect(drawNavmeshes);
|
||||
game.mapZonesChanged.connect(drawMapZones);
|
||||
game.unculledZonesChanged.connect(drawUnculledZones);
|
||||
game.selectedFlightChanged.connect(updateSelectedFlight);
|
||||
@ -400,6 +399,9 @@ function handleStreamedEvents(events) {
|
||||
for (const combat of events.updated_combats) {
|
||||
redrawCombat(combat);
|
||||
}
|
||||
for (const player of events.navmesh_updates) {
|
||||
drawNavmesh(player);
|
||||
}
|
||||
}
|
||||
|
||||
function recenterMap(center) {
|
||||
@ -1094,26 +1096,27 @@ function drawThreatZones() {
|
||||
);
|
||||
}
|
||||
|
||||
function drawNavmesh(zones, layer) {
|
||||
for (const zone of zones) {
|
||||
L.polyline(zone.poly, {
|
||||
color: "#000000",
|
||||
weight: 1,
|
||||
fillColor: zone.threatened ? "#ff0000" : "#00ff00",
|
||||
fill: true,
|
||||
fillOpacity: 0.1,
|
||||
noClip: true,
|
||||
interactive: false,
|
||||
}).addTo(layer);
|
||||
}
|
||||
function drawNavmesh(player) {
|
||||
const layer = player ? blueNavmesh : redNavmesh;
|
||||
layer.clearLayers();
|
||||
getJson(`/navmesh?for_player=${player}`).then((zones) => {
|
||||
for (const zone of zones) {
|
||||
L.polyline(zone.poly, {
|
||||
color: "#000000",
|
||||
weight: 1,
|
||||
fillColor: zone.threatened ? "#ff0000" : "#00ff00",
|
||||
fill: true,
|
||||
fillOpacity: 0.1,
|
||||
noClip: true,
|
||||
interactive: false,
|
||||
}).addTo(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawNavmeshes() {
|
||||
blueNavmesh.clearLayers();
|
||||
redNavmesh.clearLayers();
|
||||
|
||||
drawNavmesh(game.navmeshes.blue, blueNavmesh);
|
||||
drawNavmesh(game.navmeshes.red, redNavmesh);
|
||||
drawNavmesh(true);
|
||||
drawNavmesh(false);
|
||||
}
|
||||
|
||||
function drawMapZones() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user