Initial squadrons implementation.

Doesn't actually do anything yet, but squadrons are created for each
aircraft type and pilots will be created as needed to fill flights.

https://github.com/dcs-liberation/dcs_liberation/issues/276
This commit is contained in:
Dan Albert
2021-05-25 00:12:14 -07:00
parent 6b30f47588
commit 4147d2f684
22 changed files with 728 additions and 41 deletions

View File

@@ -1459,6 +1459,13 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
return None
def flying_type_from_name(name: str) -> Optional[Type[FlyingType]]:
unit_type = plane_map.get(name)
if unit_type is not None:
return unit_type
return helicopter_map.get(name)
def unit_type_of(unit: Unit) -> UnitType:
if isinstance(unit, Vehicle):
return vehicle_map[unit.type]

View File

@@ -27,6 +27,9 @@ from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@dataclass
class Faction:
#: List of locales to use when generating random names. If not set, Faker will
#: choose the default locale.
locales: Optional[List[str]]
# Country used by this faction
country: str = field(default="")
@@ -132,8 +135,7 @@ class Faction:
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
faction = Faction()
faction = Faction(locales=json.get("locales"))
faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]:

View File

@@ -4,12 +4,13 @@ import random
import sys
from datetime import date, datetime, timedelta
from enum import Enum
from typing import Any, Dict, List
from typing import Any, Dict, List, Iterator
from dcs.action import Coalition
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from faker import Faker
from game import db
from game.inventory import GlobalAircraftInventory
@@ -33,6 +34,7 @@ from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration
from .settings import Settings
from .squadrons import Pilot, AirWing
from .theater import ConflictTheater
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
@@ -140,6 +142,12 @@ class Game:
self.sanitize_sides()
self.blue_faker = Faker(self.player_faction.locales)
self.red_faker = Faker(self.enemy_faction.locales)
self.blue_air_wing = AirWing(self, player=True)
self.red_air_wing = AirWing(self, player=False)
self.on_load()
def __getstate__(self) -> Dict[str, Any]:
@@ -150,6 +158,8 @@ class Game:
del state["red_threat_zone"]
del state["blue_navmesh"]
del state["red_navmesh"]
del state["blue_faker"]
del state["red_faker"]
return state
def __setstate__(self, state: Dict[str, Any]) -> None:
@@ -205,6 +215,16 @@ class Game:
return self.player_faction
return self.enemy_faction
def faker_for(self, player: bool) -> Faker:
if player:
return self.blue_faker
return self.red_faker
def air_wing_for(self, player: bool) -> AirWing:
if player:
return self.blue_air_wing
return self.red_air_wing
def country_for(self, player: bool) -> str:
if player:
return self.player_country
@@ -286,6 +306,8 @@ class Game:
ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position()
self.compute_threat_zones()
self.blue_faker = Faker(self.faction_for(player=True).locales)
self.red_faker = Faker(self.faction_for(player=False).locales)
def reset_ato(self) -> None:
self.blue_ato.clear()

145
game/squadrons.py Normal file
View File

@@ -0,0 +1,145 @@
from __future__ import annotations
import itertools
from dataclasses import dataclass, field
from pathlib import Path
from typing import Type, Tuple, List, TYPE_CHECKING, Optional, Iterable
import yaml
from dcs.unittype import FlyingType
from faker import Faker
from game.db import flying_type_from_name
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
@dataclass
class PilotRecord:
missions_flown: int = field(default=0)
@dataclass
class Pilot:
name: str
player: bool = field(default=False)
alive: bool = field(default=True)
record: PilotRecord = field(default_factory=PilotRecord)
@classmethod
def random(cls, faker: Faker) -> Pilot:
return Pilot(faker.name())
@dataclass
class Squadron:
name: str
nickname: str
role: str
aircraft: Type[FlyingType]
mission_types: Tuple[FlightType, ...]
pilots: List[Pilot]
available_pilots: List[Pilot] = field(init=False, hash=False, compare=False)
# We need a reference to the Game so that we can access the Faker without needing to
# persist it to the save game, or having to reconstruct it (it's not cheap) each
# time we create or load a squadron.
game: Game = field(hash=False, compare=False)
player: bool
def __post_init__(self) -> None:
self.available_pilots = list(self.pilots)
def claim_available_pilot(self) -> Optional[Pilot]:
if not self.available_pilots:
self.enlist_new_pilots(1)
return self.available_pilots.pop()
def claim_pilot(self, pilot: Pilot) -> None:
if pilot not in self.available_pilots:
raise ValueError(
f"Cannot assign {pilot} to {self} because they are not available"
)
self.available_pilots.remove(pilot)
def return_pilot(self, pilot: Pilot) -> None:
self.available_pilots.append(pilot)
def return_pilots(self, pilots: Iterable[Pilot]) -> None:
self.available_pilots.extend(pilots)
def enlist_new_pilots(self, count: int) -> None:
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
self.pilots.extend(new_pilots)
self.available_pilots.extend(new_pilots)
def return_all_pilots(self) -> None:
self.available_pilots = self.pilots
@property
def faker(self) -> Faker:
return self.game.faker_for(self.player)
@property
def size(self) -> int:
return len(self.pilots)
def pilot_at_index(self, index: int) -> Pilot:
return self.pilots[index]
@classmethod
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
from gen.flights.flight import FlightType
with path.open() as squadron_file:
data = yaml.load(squadron_file)
unit_type = flying_type_from_name(data["aircraft"])
if unit_type is None:
raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
return Squadron(
name=data["name"],
nickname=data["nickname"],
role=data["role"],
aircraft=unit_type,
mission_types=tuple(FlightType.from_name(n) for n in data["mission_types"]),
pilots=[Pilot(n) for n in data.get("pilots", [])],
game=game,
player=player,
)
class AirWing:
def __init__(self, game: Game, player: bool) -> None:
from gen.flights.flight import FlightType
self.game = game
self.player = player
self.squadrons = {
aircraft: [
Squadron(
name=f"Squadron {num + 1:03}",
nickname="The Unremarkable",
role="Flying Squadron",
aircraft=aircraft,
mission_types=tuple(FlightType),
pilots=[],
game=game,
player=player,
)
]
for num, aircraft in enumerate(game.faction_for(player).aircrafts)
}
def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
return self.squadrons[aircraft][0]
def squadron_at_index(self, index: int) -> Squadron:
return list(itertools.chain.from_iterable(self.squadrons.values()))[index]
@property
def size(self) -> int:
return sum(len(s) for s in self.squadrons.values())

View File

@@ -251,10 +251,11 @@ class AirliftPlanner:
else:
transfer = self.transfer
player = inventory.control_point.captured
flight = Flight(
self.package,
self.game.country_for(inventory.control_point.captured),
unit_type,
self.game.country_for(player),
self.game.air_wing_for(player).squadron_for(unit_type),
flight_size,
FlightType.TRANSPORT,
self.game.settings.default_start_type,