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`
"""
# 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.
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")
for damaged_runway in debriefing.damaged_runways:
damaged_runway.damaged = True
damaged_runway.damage_runway()
# ------------------------------
# Destroyed aircrafts

View File

@ -26,7 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .infos.information import Information
from .settings import Settings
from .theater import ConflictTheater, ControlPoint, OffMapSpawn
from .theater import Airfield, ConflictTheater, ControlPoint, OffMapSpawn
from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay
@ -212,6 +212,9 @@ class Game:
else:
event.skip()
for control_point in self.theater.controlpoints:
control_point.process_turn()
self._enemy_reinforcement()
self._budget_player()
@ -269,9 +272,20 @@ class Game:
if g.category in REWARDS.keys() and not g.is_dead:
production = production + REWARDS[g.category]
production = production * 0.75
budget_for_armored_units = production / 2
budget_for_aircraft = production / 2
# TODO: Why doesn't the enemy get the full budget?
budget = production * 0.75
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 = []
for cp in self.theater.enemy_points():

View File

@ -148,6 +148,45 @@ class PendingOccupancy:
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):
position = None # type: Point
@ -367,6 +406,26 @@ class ControlPoint(MissionTarget, ABC):
def parking_slots(self) -> Iterator[ParkingSlot]:
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):
@ -376,7 +435,7 @@ class Airfield(ControlPoint):
size, importance, has_frontline,
cptype=ControlPointType.AIRBASE)
self.airport = airport
self.damaged = False
self._runway_status = RunwayStatus()
def can_land(self, aircraft: FlyingType) -> bool:
return True
@ -404,7 +463,14 @@ class Airfield(ControlPoint):
return self.airport.runways[0].heading
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,
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
@ -457,6 +523,14 @@ class NavalControlPoint(ControlPoint, ABC):
fallback = RunwayData(self.full_name, runway_heading=0, runway_name="")
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):
@ -537,3 +611,7 @@ class OffMapSpawn(ControlPoint):
dynamic_runways: Dict[str, RunwayData]) -> RunwayData:
logging.warning("TODO: Off map spawns have no runways.")
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,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
)
from game import db
from game.theater import ControlPoint, ControlPointType
from gen.flights.flight import FlightType
from qt_ui.dialogs import Dialog
@ -59,17 +61,17 @@ class QBaseMenu2(QDialog):
title = QLabel("<b>" + self.cp.name + "</b>")
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title")
aircraft = self.cp.base.total_aircraft
armor = self.cp.base.total_armor
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}"
]))
self.intel_summary = QLabel()
self.update_intel_summary()
top_layout.addWidget(title)
top_layout.addWidget(intel_summary)
top_layout.addWidget(self.intel_summary)
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.setLayout(top_layout)
@ -96,7 +98,66 @@ class QBaseMenu2(QDialog):
bottom_row.addWidget(budget_display)
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)
def get_base_image(self):