mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Add a new miz file based campaign generator.
Defining a campaign using a miz file instead of as JSON has a number of advantages: * Much easier for players to mod their campaigns. * Easier to see the big picture of how objective locations will be laid out, since every control point can be seen at once. * No need to associate objective locations to control points explicitly; the campaign generator can claim objectives for control points based on distance. * Easier to create an IADS that performs well. * Non-random campaigns are easier to make. The downside is duplication across campaigns, and a less structured data format for complex objects. The former is annoying if we have to fix a bug that appears in a dozen campaigns. It's less an annoyance for needing to start from scratch since the easiest way to create a campaign will be to copy the "full" campaign for the given theater and prune it. So far I've implemented control points, base defenses, and front lines. Still need to add support for non-base defense TGOs. This currently doesn't do anything for the `radials` property of the `ControlPoint` because I'm not sure what those are.
This commit is contained in:
parent
20f97e48a9
commit
df80ec635f
@ -1,13 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from itertools import tee
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.countries import (
|
||||
CombinedJointTaskForcesBlue,
|
||||
CombinedJointTaskForcesRed,
|
||||
)
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import CVN_74_John_C__Stennis, LHA_1_Tarawa
|
||||
from dcs.terrain import (
|
||||
caucasus,
|
||||
nevada,
|
||||
@ -16,11 +25,14 @@ from dcs.terrain import (
|
||||
syria,
|
||||
thechannel,
|
||||
)
|
||||
from dcs.terrain.terrain import Terrain
|
||||
from dcs.terrain.terrain import Airport, Terrain
|
||||
from dcs.unitgroup import MovingGroup, ShipGroup, VehicleGroup
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
from .controlpoint import ControlPoint, MissionTarget
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from ..utils import nm_to_meter
|
||||
|
||||
Numeric = Union[int, float]
|
||||
|
||||
@ -73,6 +85,193 @@ def pairwise(iterable):
|
||||
return zip(a, b)
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
||||
RED_COUNTRY = CombinedJointTaskForcesRed()
|
||||
|
||||
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_1_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
|
||||
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
|
||||
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
|
||||
|
||||
BASE_DEFENSE_RADIUS = nm_to_meter(2)
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
self.mission = Mission()
|
||||
self.mission.load_file(str(miz))
|
||||
self.control_point_id = itertools.count(1000)
|
||||
|
||||
# If there are no red carriers there usually aren't red units. Make sure
|
||||
# both countries are initialized so we don't have to deal with None.
|
||||
if self.mission.country(self.BLUE_COUNTRY.name) is None:
|
||||
self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
|
||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||
|
||||
@staticmethod
|
||||
def control_point_from_airport(airport: Airport) -> ControlPoint:
|
||||
# TODO: Radials?
|
||||
radials = LAND
|
||||
|
||||
# The wiki says this is a legacy property and to just use regular.
|
||||
size = SIZE_REGULAR
|
||||
|
||||
# The importance is taken from the periodicity of the airport's
|
||||
# warehouse divided by 10. 30 is the default, and out of range (valid
|
||||
# values are between 1.0 and 1.4). If it is used, pick the default
|
||||
# importance.
|
||||
if airport.periodicity == 30:
|
||||
importance = IMPORTANCE_MEDIUM
|
||||
else:
|
||||
importance = airport.periodicity / 10
|
||||
|
||||
cp = ControlPoint.from_airport(airport, radials, size, importance)
|
||||
cp.captured = airport.is_blue()
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
cp.captured_invert = airport.unlimited_aircrafts
|
||||
|
||||
return cp
|
||||
|
||||
def country(self, blue: bool) -> Country:
|
||||
country = self.mission.country(
|
||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name)
|
||||
# Should be guaranteed because we initialized them.
|
||||
assert country
|
||||
return country
|
||||
|
||||
@property
|
||||
def blue(self) -> Country:
|
||||
return self.country(blue=True)
|
||||
|
||||
@property
|
||||
def red(self) -> Country:
|
||||
return self.country(blue=False)
|
||||
|
||||
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.CV_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.SAM_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def garrisons(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.GARRISON_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def control_points(self) -> Dict[int, ControlPoint]:
|
||||
control_points = {}
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.is_blue() or airport.is_red():
|
||||
control_point = self.control_point_from_airport(airport)
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for blue in (False, True):
|
||||
for group in self.carriers(blue):
|
||||
control_point = ControlPoint.carrier(
|
||||
"carrier", group.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.lhas(blue):
|
||||
control_point = ControlPoint.lha(
|
||||
"lha", group.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
return control_points
|
||||
|
||||
@property
|
||||
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue=True).vehicle_group:
|
||||
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@cached_property
|
||||
def front_lines(self) -> Dict[str, ComplexFrontLine]:
|
||||
# Dict of front line ID to a front line.
|
||||
front_lines = {}
|
||||
for group in self.front_line_path_groups:
|
||||
# The unit will have its first waypoint at the source CP and the
|
||||
# final waypoint at the destination CP. Intermediate waypoints
|
||||
# define the curve of the front line.
|
||||
waypoints = [p.position for p in group.points]
|
||||
origin = self.mission.terrain.nearest_airport(waypoints[0])
|
||||
if origin is None:
|
||||
raise RuntimeError(
|
||||
f"No airport near the first waypoint of {group.name}")
|
||||
destination = self.mission.terrain.nearest_airport(waypoints[-1])
|
||||
if destination is None:
|
||||
raise RuntimeError(
|
||||
f"No airport near the final waypoint of {group.name}")
|
||||
|
||||
# Snap the begin and end points to the control points.
|
||||
waypoints[0] = origin.position
|
||||
waypoints[-1] = destination.position
|
||||
front_line_id = f"{origin.id}|{destination.id}"
|
||||
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
|
||||
self.control_points[origin.id].connect(
|
||||
self.control_points[destination.id])
|
||||
self.control_points[destination.id].connect(
|
||||
self.control_points[origin.id])
|
||||
return front_lines
|
||||
|
||||
def objective_info(self, group: MovingGroup) -> Tuple[ControlPoint, int]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = closest.position.distance_to_point(group.position)
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
for group in self.garrisons:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_garrisons.append(group.position)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Found garrison unit too far from base: {group.name}")
|
||||
|
||||
for group in self.sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_air_defense.append(group.position)
|
||||
else:
|
||||
closest.preset_locations.sams.append(group.position)
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ewrs.append(group.position)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
for control_point in self.control_points.values():
|
||||
self.theater.add_controlpoint(control_point)
|
||||
self.add_preset_locations()
|
||||
self.theater.set_frontline_data(self.front_lines)
|
||||
|
||||
|
||||
class ConflictTheater:
|
||||
terrain: Terrain
|
||||
|
||||
@ -83,17 +282,35 @@ class ConflictTheater:
|
||||
land_poly = None # type: Polygon
|
||||
"""
|
||||
daytime_map: Dict[str, Tuple[int, int]]
|
||||
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
_frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
|
||||
def __init__(self):
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
self.frontline_data = FrontLine.load_json_frontlines(self)
|
||||
self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
"""
|
||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||
for x in self.landmap[1]:
|
||||
self.land_poly = self.land_poly.difference(geometry.Polygon(x))
|
||||
"""
|
||||
|
||||
@property
|
||||
def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
|
||||
if self._frontline_data is None:
|
||||
self.load_frontline_data_from_file()
|
||||
return self._frontline_data
|
||||
|
||||
def load_frontline_data_from_file(self) -> None:
|
||||
if self._frontline_data is not None:
|
||||
logging.warning("Replacing existing frontline data from file")
|
||||
self._frontline_data = FrontLine.load_json_frontlines(self)
|
||||
if self._frontline_data is None:
|
||||
self._frontline_data = {}
|
||||
|
||||
def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
|
||||
if self._frontline_data is not None:
|
||||
logging.warning("Replacing existing frontline data")
|
||||
self._frontline_data = data
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint,
|
||||
connected_to: Optional[List[ControlPoint]] = None):
|
||||
if connected_to is None:
|
||||
@ -153,11 +370,21 @@ class ConflictTheater:
|
||||
def enemy_points(self) -> List[ControlPoint]:
|
||||
return [point for point in self.controlpoints if not point.captured]
|
||||
|
||||
def closest_control_point(self, point: Point) -> ControlPoint:
|
||||
closest = self.controlpoints[0]
|
||||
closest_distance = point.distance_to_point(closest.position)
|
||||
for control_point in self.controlpoints[1:]:
|
||||
distance = point.distance_to_point(control_point.position)
|
||||
if distance < closest_distance:
|
||||
closest = control_point
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
def add_json_cp(self, theater, p: dict) -> ControlPoint:
|
||||
|
||||
if p["type"] == "airbase":
|
||||
|
||||
airbase = theater.terrain.airports[p["id"]].__class__
|
||||
airbase = theater.terrain.airports[p["id"]]
|
||||
|
||||
if "radials" in p.keys():
|
||||
radials = p["radials"]
|
||||
@ -188,7 +415,7 @@ class ConflictTheater:
|
||||
return cp
|
||||
|
||||
@staticmethod
|
||||
def from_json(data: Dict[str, Any]) -> ConflictTheater:
|
||||
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
|
||||
theaters = {
|
||||
"Caucasus": CaucasusTheater,
|
||||
"Nevada": NevadaTheater,
|
||||
@ -199,6 +426,12 @@ class ConflictTheater:
|
||||
}
|
||||
theater = theaters[data["theater"]]
|
||||
t = theater()
|
||||
|
||||
miz = data.get("miz", None)
|
||||
if miz is not None:
|
||||
MizCampaignLoader(directory / miz, t).populate_theater()
|
||||
return t
|
||||
|
||||
cps = {}
|
||||
for p in data["player_points"]:
|
||||
cp = t.add_json_cp(theater, p)
|
||||
@ -376,10 +609,6 @@ class FrontLine(MissionTarget):
|
||||
"""Returns a tuple of the two control points."""
|
||||
return self.control_point_a, self.control_point_b
|
||||
|
||||
@property
|
||||
def middle_point(self):
|
||||
self.point_from_a(self.attack_distance / 2)
|
||||
|
||||
@property
|
||||
def attack_distance(self):
|
||||
"""The total distance of all segments"""
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import random
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Iterator, List, TYPE_CHECKING
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import (
|
||||
@ -36,6 +38,43 @@ class ControlPointType(Enum):
|
||||
FOB = 5 # A FOB (ground units only)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocations:
|
||||
base_garrisons: List[Point] = field(default_factory=list)
|
||||
base_air_defense: List[Point] = field(default_factory=list)
|
||||
|
||||
ewrs: List[Point] = field(default_factory=list)
|
||||
sams: List[Point] = field(default_factory=list)
|
||||
coastal_defenses: List[Point] = field(default_factory=list)
|
||||
strike_locations: List[Point] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def _random_from(points: List[Point]) -> Optional[Point]:
|
||||
if not points:
|
||||
return None
|
||||
point = random.choice(points)
|
||||
points.remove(point)
|
||||
return point
|
||||
|
||||
def random_garrison(self) -> Optional[Point]:
|
||||
return self._random_from(self.base_garrisons)
|
||||
|
||||
def random_base_sam(self) -> Optional[Point]:
|
||||
return self._random_from(self.base_air_defense)
|
||||
|
||||
def random_ewr(self) -> Optional[Point]:
|
||||
return self._random_from(self.ewrs)
|
||||
|
||||
def random_sam(self) -> Optional[Point]:
|
||||
return self._random_from(self.sams)
|
||||
|
||||
def random_coastal_defense(self) -> Optional[Point]:
|
||||
return self._random_from(self.coastal_defenses)
|
||||
|
||||
def random_strike_location(self) -> Optional[Point]:
|
||||
return self._random_from(self.strike_locations)
|
||||
|
||||
|
||||
class ControlPoint(MissionTarget):
|
||||
|
||||
position = None # type: Point
|
||||
@ -57,6 +96,7 @@ class ControlPoint(MissionTarget):
|
||||
self.at = at
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.base_defenses: List[BaseDefenseGroundObject] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
|
||||
self.size = size
|
||||
self.importance = importance
|
||||
@ -79,7 +119,7 @@ class ControlPoint(MissionTarget):
|
||||
def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True):
|
||||
assert airport
|
||||
obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE)
|
||||
obj.airport = airport()
|
||||
obj.airport = airport
|
||||
return obj
|
||||
|
||||
@classmethod
|
||||
@ -157,7 +197,7 @@ class ControlPoint(MissionTarget):
|
||||
else:
|
||||
return 0
|
||||
|
||||
def connect(self, to):
|
||||
def connect(self, to: ControlPoint) -> None:
|
||||
self.connected_points.append(to)
|
||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
import math
|
||||
import pickle
|
||||
import random
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
@ -13,6 +13,17 @@ from dcs.vehicles import AirDefence
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
EwrGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
SamGroundObject,
|
||||
ShipGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.version import VERSION
|
||||
from gen import namegen
|
||||
from gen.defenses.armor_group_generator import generate_armor_group
|
||||
@ -34,17 +45,6 @@ from theater import (
|
||||
ControlPointType,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from game.theater.theatergroundobject import (
|
||||
EwrGroundObject,
|
||||
SamGroundObject,
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
ShipGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
|
||||
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
||||
|
||||
@ -317,10 +317,9 @@ class BaseDefenseGenerator:
|
||||
self.generate_base_defenses()
|
||||
|
||||
def generate_ewr(self) -> None:
|
||||
position = self._find_location()
|
||||
position = self._find_location(
|
||||
"EWR", self.control_point.preset_locations.random_ewr)
|
||||
if position is None:
|
||||
logging.error("Could not find position for "
|
||||
f"{self.control_point} EWR")
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
@ -350,10 +349,9 @@ class BaseDefenseGenerator:
|
||||
self.generate_garrison()
|
||||
|
||||
def generate_garrison(self) -> None:
|
||||
position = self._find_location()
|
||||
position = self._find_location(
|
||||
"garrison", self.control_point.preset_locations.random_garrison)
|
||||
if position is None:
|
||||
logging.error("Could not find position for "
|
||||
f"{self.control_point} garrison")
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
@ -368,10 +366,9 @@ class BaseDefenseGenerator:
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_sam(self) -> None:
|
||||
position = self._find_location()
|
||||
position = self._find_location(
|
||||
"SAM", self.control_point.preset_locations.random_base_sam)
|
||||
if position is None:
|
||||
logging.error("Could not find position for "
|
||||
f"{self.control_point} SAM")
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
@ -385,10 +382,9 @@ class BaseDefenseGenerator:
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
position = self._find_location()
|
||||
position = self._find_location(
|
||||
"SHORAD", self.control_point.preset_locations.random_garrison)
|
||||
if position is None:
|
||||
logging.error("Could not find position for "
|
||||
f"{self.control_point} SHORAD")
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
@ -401,7 +397,21 @@ class BaseDefenseGenerator:
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def _find_location(self) -> Optional[Point]:
|
||||
def _find_location(self, position_type: str,
|
||||
get_preset: Callable[[], None]) -> Optional[Point]:
|
||||
position = get_preset()
|
||||
if position is None:
|
||||
logging.warning(
|
||||
f"Found no preset location for {self.control_point} "
|
||||
f"{position_type}. Falling back to random location."
|
||||
)
|
||||
position = self._find_random_location()
|
||||
if position is None:
|
||||
logging.error("Could not find position for "
|
||||
f"{self.control_point} {position_type}.")
|
||||
return position
|
||||
|
||||
def _find_random_location(self) -> Optional[Point]:
|
||||
position = find_location(True, self.control_point.position,
|
||||
self.game.theater, 400, 3200, [], True)
|
||||
|
||||
|
||||
@ -29,8 +29,10 @@ class Campaign:
|
||||
data = json.load(campaign_file)
|
||||
|
||||
sanitized_theater = data["theater"].replace(" ", "")
|
||||
return cls(data["name"], f"Terrain_{sanitized_theater}", data.get("authors", "???"),
|
||||
data.get("description", ""), ConflictTheater.from_json(data))
|
||||
return cls(data["name"], f"Terrain_{sanitized_theater}",
|
||||
data.get("authors", "???"),
|
||||
data.get("description", ""),
|
||||
ConflictTheater.from_json(path.parent, data))
|
||||
|
||||
|
||||
def load_campaigns() -> List[Campaign]:
|
||||
|
||||
@ -3,82 +3,5 @@
|
||||
"theater": "Syria",
|
||||
"authors": "Khopa",
|
||||
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
|
||||
"player_points": [
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "King Hussein Air College",
|
||||
"size": 1000,
|
||||
"importance": 1.4
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Incirlik",
|
||||
"size": 1000,
|
||||
"importance": 1.4,
|
||||
"captured_invert": true
|
||||
},
|
||||
{
|
||||
"type": "carrier",
|
||||
"id": 1001,
|
||||
"x": -210000,
|
||||
"y": -200000,
|
||||
"captured_invert": true
|
||||
},
|
||||
{
|
||||
"type": "lha",
|
||||
"id": 1002,
|
||||
"x": -131000,
|
||||
"y": -161000,
|
||||
"captured_invert": true
|
||||
}
|
||||
],
|
||||
"enemy_points": [
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Khalkhalah",
|
||||
"size": 1000,
|
||||
"importance": 1.2
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Palmyra",
|
||||
"size": 1000,
|
||||
"importance": 1
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Tabqa",
|
||||
"size": 1000,
|
||||
"importance": 1
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Jirah",
|
||||
"size": 1000,
|
||||
"importance": 1,
|
||||
"captured_invert": true
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
"Khalkhalah",
|
||||
"King Hussein Air College"
|
||||
],
|
||||
[
|
||||
"Incirlik",
|
||||
"Incirlik"
|
||||
],
|
||||
[
|
||||
"Khalkhalah",
|
||||
"Palmyra"
|
||||
],
|
||||
[
|
||||
"Palmyra",
|
||||
"Tabqa"
|
||||
],
|
||||
[
|
||||
"Jirah",
|
||||
"Tabqa"
|
||||
]
|
||||
]
|
||||
"miz": "inherent_resolve.miz"
|
||||
}
|
||||
BIN
resources/campaigns/inherent_resolve.miz
Normal file
BIN
resources/campaigns/inherent_resolve.miz
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user