Support reparing damaged runways.

Repairing a damaged runway costs $100M and takes 4 turns (one day). The
AI will always repair runways if they can afford it. if a runway is
damaged again during the repair the process must begin again.

Runways are still operational despite what the UI says. Preventing the
player and AI from using damaged runways (except for with helicopters
and harriers) is next.
This commit is contained in:
Dan Albert 2020-11-25 17:34:34 -08:00
parent ee768b9147
commit 80bc9d6b23
5 changed files with 175 additions and 17 deletions

View File

@ -225,6 +225,11 @@ from this example `Identifier` should be used (which may or may not include cate
For example, player accessible Hornet is called `FA_18C_hornet`, and MANPAD Igla is called `AirDefence.SAM_SA_18_Igla_S_MANPADS` For example, player accessible Hornet is called `FA_18C_hornet`, and MANPAD Igla is called `AirDefence.SAM_SA_18_Igla_S_MANPADS`
""" """
# This should probably be much higher, but the AI doesn't rollover their budget
# and isn't smart enough to save to repair a critical runway anyway, so it has
# to be cheap enough to repair with a single turn's income.
RUNWAY_REPAIR_COST = 100
""" """
Prices for the aircraft. Prices for the aircraft.
This defines both price for the player (although only aircraft listed in CAP/CAS/Transport/Armor/AirDefense roles will be purchasable) This defines both price for the player (although only aircraft listed in CAP/CAS/Transport/Armor/AirDefense roles will be purchasable)

View File

@ -118,7 +118,7 @@ class Event:
logging.info("Commiting mission results") logging.info("Commiting mission results")
for damaged_runway in debriefing.damaged_runways: for damaged_runway in debriefing.damaged_runways:
damaged_runway.damaged = True damaged_runway.damage_runway()
# ------------------------------ # ------------------------------
# Destroyed aircrafts # Destroyed aircrafts

View File

@ -26,7 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction from .factions.faction import Faction
from .infos.information import Information from .infos.information import Information
from .settings import Settings from .settings import Settings
from .theater import ConflictTheater, ControlPoint, OffMapSpawn from .theater import Airfield, ConflictTheater, ControlPoint, OffMapSpawn
from .unitmap import UnitMap from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay from .weather import Conditions, TimeOfDay
@ -212,6 +212,9 @@ class Game:
else: else:
event.skip() event.skip()
for control_point in self.theater.controlpoints:
control_point.process_turn()
self._enemy_reinforcement() self._enemy_reinforcement()
self._budget_player() self._budget_player()
@ -269,9 +272,20 @@ class Game:
if g.category in REWARDS.keys() and not g.is_dead: if g.category in REWARDS.keys() and not g.is_dead:
production = production + REWARDS[g.category] production = production + REWARDS[g.category]
production = production * 0.75 # TODO: Why doesn't the enemy get the full budget?
budget_for_armored_units = production / 2 budget = production * 0.75
budget_for_aircraft = production / 2
for control_point in self.theater.enemy_points():
if budget < db.RUNWAY_REPAIR_COST:
break
if control_point.runway_can_be_repaired:
control_point.begin_runway_repair()
budget -= db.RUNWAY_REPAIR_COST
self.informations.append(Information(
f"OPFOR has begun repairing the runway at {control_point}"))
budget_for_armored_units = budget / 2
budget_for_aircraft = budget / 2
potential_cp_armor = [] potential_cp_armor = []
for cp in self.theater.enemy_points(): for cp in self.theater.enemy_points():

View File

@ -148,6 +148,45 @@ class PendingOccupancy:
return self.present + self.ordered + self.transferring return self.present + self.ordered + self.transferring
@dataclass
class RunwayStatus:
damaged: bool = False
repair_turns_remaining: Optional[int] = None
def damage(self) -> None:
self.damaged = True
# If the runway is already under repair and is damaged again, progress
# is reset.
self.repair_turns_remaining = None
def begin_repair(self) -> None:
if self.repair_turns_remaining is not None:
logging.error("Runway already under repair. Restarting.")
self.repair_turns_remaining = 4
def process_turn(self) -> None:
if self.repair_turns_remaining is not None:
if self.repair_turns_remaining == 1:
self.repair_turns_remaining = None
self.damaged = False
else:
self.repair_turns_remaining -= 1
@property
def needs_repair(self) -> bool:
return self.damaged and self.repair_turns_remaining is None
def __str__(self) -> str:
if not self.damaged:
return "Runway operational"
turns_remaining = self.repair_turns_remaining
if turns_remaining is None:
return "Runway damaged"
return f"Runway repairing, {turns_remaining} turns remaining"
class ControlPoint(MissionTarget, ABC): class ControlPoint(MissionTarget, ABC):
position = None # type: Point position = None # type: Point
@ -367,6 +406,26 @@ class ControlPoint(MissionTarget, ABC):
def parking_slots(self) -> Iterator[ParkingSlot]: def parking_slots(self) -> Iterator[ParkingSlot]:
yield from [] yield from []
@property
@abstractmethod
def runway_status(self) -> RunwayStatus:
...
@property
def runway_can_be_repaired(self) -> bool:
return self.runway_status.needs_repair
def begin_runway_repair(self) -> None:
if not self.runway_can_be_repaired:
logging.error(f"Cannot repair runway at {self}")
return
self.runway_status.begin_repair()
def process_turn(self) -> None:
runway_status = self.runway_status
if runway_status is not None:
runway_status.process_turn()
class Airfield(ControlPoint): class Airfield(ControlPoint):
@ -376,7 +435,7 @@ class Airfield(ControlPoint):
size, importance, has_frontline, size, importance, has_frontline,
cptype=ControlPointType.AIRBASE) cptype=ControlPointType.AIRBASE)
self.airport = airport self.airport = airport
self.damaged = False self._runway_status = RunwayStatus()
def can_land(self, aircraft: FlyingType) -> bool: def can_land(self, aircraft: FlyingType) -> bool:
return True return True
@ -404,7 +463,14 @@ class Airfield(ControlPoint):
return self.airport.runways[0].heading return self.airport.runways[0].heading
def has_runway(self) -> bool: def has_runway(self) -> bool:
return not self.damaged return not self.runway_status.damaged
@property
def runway_status(self) -> RunwayStatus:
return self._runway_status
def damage_runway(self) -> None:
self.runway_status.damage()
def active_runway(self, conditions: Conditions, def active_runway(self, conditions: Conditions,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
@ -457,6 +523,14 @@ class NavalControlPoint(ControlPoint, ABC):
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="") fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
return dynamic_runways.get(self.name, fallback) return dynamic_runways.get(self.name, fallback)
@property
def runway_status(self) -> RunwayStatus:
return RunwayStatus(damaged=not self.has_runway())
@property
def runway_can_be_repaired(self) -> bool:
return False
class Carrier(NavalControlPoint): class Carrier(NavalControlPoint):
@ -537,3 +611,7 @@ class OffMapSpawn(ControlPoint):
dynamic_runways: Dict[str, RunwayData]) -> RunwayData: dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.") logging.warning("TODO: Off map spawns have no runways.")
return RunwayData(self.full_name, runway_heading=0, runway_name="") return RunwayData(self.full_name, runway_heading=0, runway_name="")
@property
def runway_status(self) -> RunwayStatus:
return RunwayStatus()

View File

@ -4,11 +4,13 @@ from PySide2.QtWidgets import (
QDialog, QDialog,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QMessageBox,
QPushButton, QPushButton,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
) )
from game import db
from game.theater import ControlPoint, ControlPointType from game.theater import ControlPoint, ControlPointType
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
@ -59,17 +61,17 @@ class QBaseMenu2(QDialog):
title = QLabel("<b>" + self.cp.name + "</b>") title = QLabel("<b>" + self.cp.name + "</b>")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop) title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title") title.setProperty("style", "base-title")
aircraft = self.cp.base.total_aircraft self.intel_summary = QLabel()
armor = self.cp.base.total_armor self.update_intel_summary()
runway_status = "operational" if self.cp.has_runway() else "damaged"
intel_summary = QLabel("\n".join([
f"{aircraft} aircraft",
f"{armor} ground units",
f"Runway {runway_status}"
]))
top_layout.addWidget(title) top_layout.addWidget(title)
top_layout.addWidget(intel_summary) top_layout.addWidget(self.intel_summary)
top_layout.setAlignment(Qt.AlignTop) top_layout.setAlignment(Qt.AlignTop)
self.repair_button = QPushButton()
self.repair_button.clicked.connect(self.begin_runway_repair)
self.update_repair_button()
top_layout.addWidget(self.repair_button)
base_menu_header.setProperty("style", "baseMenuHeader") base_menu_header.setProperty("style", "baseMenuHeader")
base_menu_header.setLayout(top_layout) base_menu_header.setLayout(top_layout)
@ -96,7 +98,66 @@ class QBaseMenu2(QDialog):
bottom_row.addWidget(budget_display) bottom_row.addWidget(budget_display)
self.setLayout(main_layout) self.setLayout(main_layout)
def closeEvent(self, closeEvent:QCloseEvent): @property
def can_repair_runway(self) -> bool:
return self.cp.captured and self.cp.runway_can_be_repaired
@property
def can_afford_runway_repair(self) -> bool:
return self.game_model.game.budget >= db.RUNWAY_REPAIR_COST
def begin_runway_repair(self) -> None:
if not self.can_afford_runway_repair:
QMessageBox.critical(
self,
"Cannot repair runway",
f"Runway repair costs ${db.RUNWAY_REPAIR_COST}M but you have "
f"only ${self.game_model.game.budget}M available.",
QMessageBox.Ok)
return
if not self.can_repair_runway:
QMessageBox.critical(
self,
"Cannot repair runway",
f"Cannot repair this runway.", QMessageBox.Ok)
return
self.cp.begin_runway_repair()
self.game_model.game.budget -= db.RUNWAY_REPAIR_COST
self.update_repair_button()
self.update_intel_summary()
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
def update_repair_button(self) -> None:
self.repair_button.setVisible(True)
turns_remaining = self.cp.runway_status.repair_turns_remaining
if self.cp.captured and turns_remaining is not None:
self.repair_button.setText("Repairing...")
self.repair_button.setDisabled(True)
return
if self.can_repair_runway:
if self.can_afford_runway_repair:
self.repair_button.setText(f"Repair ${db.RUNWAY_REPAIR_COST}M")
self.repair_button.setDisabled(False)
return
else:
self.repair_button.setText(
f"Cannot afford repair ${db.RUNWAY_REPAIR_COST}M")
self.repair_button.setDisabled(True)
return
self.repair_button.setVisible(False)
self.repair_button.setDisabled(True)
def update_intel_summary(self) -> None:
self.intel_summary.setText("\n".join([
f"{self.cp.base.total_aircraft} aircraft",
f"{self.cp.base.total_armor} ground units",
str(self.cp.runway_status)
]))
def closeEvent(self, close_event: QCloseEvent):
GameUpdateSignal.get_instance().updateGame(self.game_model.game) GameUpdateSignal.get_instance().updateGame(self.game_model.game)
def get_base_image(self): def get_base_image(self):