from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import pygame
from nqp.base_classes.controller import Controller
from nqp.command.troupe import Troupe
from nqp.core.constants import BARRIER_SIZE, CombatState, GameSpeed, TILE_SIZE, WorldState
from nqp.core.debug import Timer
if TYPE_CHECKING:
from typing import Any, Dict, List
from nqp.core.game import Game
from nqp.scenes.world.scene import WorldScene
__all__ = ["CombatController"]
[docs]class CombatController(Controller):
"""
Combat game functionality and combat only data.
* Modify game state in accordance with game rules
* Do not draw anything
"""
[docs] def __init__(self, game: Game, parent_scene: WorldScene):
with Timer("CombatController: initialised"):
super().__init__(game, parent_scene)
self.victory_duration: float = 0.0
self._combat_ending_timer: float = -1
self.last_unit_death: List = list()
# state
self.combat_category: str = "basic"
self.enemy_troupe_id: int = -1
self.state: CombatState = CombatState.IDLE
self._game_log: List[str] = [] # TODO - needs moving to model
[docs] def update(self, delta_time: float):
# do nothing if in idle
if self.state == CombatState.IDLE:
return
# if combat ending
if self._combat_ending_timer != -1:
self._combat_ending_timer += delta_time
self._game.memory.set_game_speed(0.3 - (0.05 * self._combat_ending_timer))
# self.ui.camera.zoom = 1 + (self._combat_ending_timer / 2)
for troupe in self._parent_scene.model.troupes.values():
troupe.set_force_idle(True)
# end combat when either side is empty
if (self._parent_scene.model.state == WorldState.COMBAT) and (self._combat_ending_timer == -1):
all_entities = self._parent_scene.model.get_all_entities()
player_entities = [e for e in all_entities if e.team == "player"]
if len(player_entities) == 0:
self._process_defeat()
elif len(player_entities) == len(all_entities):
self._process_victory()
if self.state == CombatState.VICTORY:
self.victory_duration += delta_time
if self.victory_duration > 3:
self._parent_scene.model.remove_troupe(self.enemy_troupe_id)
for troupe in self._parent_scene.model.troupes.values():
troupe.set_force_idle(False)
self._parent_scene.ui.grid.move_units_to_grid()
self.end_combat()
# TODO - what is last death?
if self.last_unit_death:
# average the last positions of the last entity to die and the killer of that entity
focus_point = (
(self.last_unit_death[0].pos[0] + self.last_unit_death[1].pos[0]) / 2,
(self.last_unit_death[0].pos[1] + self.last_unit_death[1].pos[1]) / 2,
)
# TODO: decouple this
self._parent_scene.ui._worldview.camera.set_target_position(focus_point)
[docs] def prepare_combat(self):
"""
Complete combat preparation steps.
"""
self.generate_combat()
for troupe in self._parent_scene.model.troupes.values():
troupe.set_force_idle(False)
[docs] def begin_combat(self):
"""
Start active combat.
"""
self.state = CombatState.WATCH
[docs] def reset(self):
self._parent_scene.model.state = WorldState.CHOOSE_NEXT_ROOM
self._combat_ending_timer = -1
self.enemy_troupe_id = -1
[docs] def generate_combat(self):
"""
Generate a random combat
"""
rng = self._game.rng
combat = self._get_random_combat()
terrain = self._parent_scene.model.terrain
logging.debug(f"{combat['type']} combat chosen.")
num_units = len(combat["units"])
enemy_troupe = Troupe(self._game, "enemy", [])
# generate positions
minx = miny = (BARRIER_SIZE + 1) * TILE_SIZE
maxx = ((terrain.size.x // 2) - 1) * TILE_SIZE
maxy = ((terrain.size.y // 2) - 1) * TILE_SIZE
positions = []
for i in range(num_units):
# choose a random spot on the left side of the map
while True:
pos = pygame.Vector2(
rng.randint(minx, maxx),
rng.randint(miny, maxy),
)
if not terrain.check_tile_solid(pos):
break
positions.append(pos)
# generate units
if self._game.debug.debug_mode:
ids = enemy_troupe.debug_init_units()
ids = ids[:num_units]
else:
ids = enemy_troupe.generate_specific_units(unit_types=combat["units"])
# assign positions and add to combat
for id_ in ids:
unit = enemy_troupe.units[id_]
unit.set_position(positions.pop(0))
troupe_id = self._parent_scene.model.add_troupe(enemy_troupe)
self.enemy_troupe_id = troupe_id
self._combat_ending_timer = -1
def _get_random_combat(self) -> Dict[str, Any]:
"""
Return dictionary of data for a random combat
"""
if not self._game.data.combats:
return {}
# get possible combats
level = self._parent_scene.model.level
combats = self._game.data.combats.values()
possible_combats = []
possible_combats_occur_rates = []
for combat in combats:
# ensure only combat for this level or lower and of desired type
if combat["level_available"] <= level and combat["category"] == self.combat_category:
possible_combats.append(combat)
occur_rate = self._game.data.get_combat_occur_rate(combat["type"])
possible_combats_occur_rates.append(occur_rate)
return self._game.rng.choices(possible_combats, possible_combats_occur_rates)[0]
def _process_defeat(self):
"""
Process the defeat, such as removing morale
"""
self.combat_ending_timer = 0
self.state = CombatState.DEFEAT
def _process_victory(self):
"""
Process victory
"""
self.combat_ending_timer = 0
self.state = CombatState.VICTORY
[docs] def end_combat(self):
"""
End the combat
"""
self._game.memory.set_game_speed(GameSpeed.NORMAL)
for troupe in self._parent_scene.model.troupes.values():
troupe.set_force_idle(True)
self._process_new_injuries()
def _process_new_injuries(self):
"""
Process new injuries and resulting deaths
"""
remove_units = []
injuries_before_death = self._game.data.config["unit_properties"]["injuries_before_death"]
log = self._game_log.append
for i, unit in enumerate(self._parent_scene.model.player_troupe.units.values()):
# do an update to ensure unit.alive is updated
unit.update(0.0001)
# add injury for units killed in combat
if not unit.alive:
unit.injuries += 1
# remove unit from troupe if the unit took too many injuries
if unit.injuries >= injuries_before_death:
remove_units.append(unit.id)
log(f"{unit.type} died due to their injuries.")
else:
log(f"{unit.type} was injured in battle.")
# remove units after since they can't be removed during iteration
for unit in remove_units:
self._parent_scene.model.player_troupe.remove_unit(unit)