|
| 1 | +import random |
| 2 | +from time import sleep |
| 3 | + |
| 4 | +import card_catalog |
| 5 | +import displayer as view |
| 6 | +import effect_interface as ei |
| 7 | +import game_map |
| 8 | +import generators as gen |
| 9 | +import potion_catalog |
| 10 | +from ansi_tags import ansiprint |
| 11 | +from definitions import CombatTier, State, TargetType |
| 12 | +from enemy import Enemy |
| 13 | +from enemy_catalog import ( |
| 14 | + create_act1_boss, |
| 15 | + create_act1_elites, |
| 16 | + create_act1_normal_encounters, |
| 17 | +) |
| 18 | +from message_bus_tools import Message, bus |
| 19 | +from player import Player |
| 20 | + |
| 21 | + |
| 22 | +class Combat: |
| 23 | + def __init__(self, tier: CombatTier, player: Player, game_map: game_map.GameMap, all_enemies: list[Enemy] | None = None): |
| 24 | + self.tier = tier |
| 25 | + self.player = player |
| 26 | + self.all_enemies = all_enemies if all_enemies else [] |
| 27 | + self.previous_enemy_states = () |
| 28 | + self.death_messages = [] |
| 29 | + self.turn = 1 |
| 30 | + self.game_map = game_map |
| 31 | + |
| 32 | + @property |
| 33 | + def active_enemies(self): |
| 34 | + return [enemy for enemy in self.all_enemies if enemy.state == State.ALIVE] |
| 35 | + |
| 36 | + def combat(self) -> None: |
| 37 | + """There's too much to say here.""" |
| 38 | + self.start_combat() |
| 39 | + # Combat automatically ends when all enemies are dead. |
| 40 | + while len(self.active_enemies) > 0: |
| 41 | + bus.publish(Message.START_OF_TURN, (self.turn, self.player)) |
| 42 | + while True: |
| 43 | + self.on_player_move() |
| 44 | + if all((enemy.state == State.DEAD for enemy in self.all_enemies)): |
| 45 | + self.end_combat(killed_enemies=True) |
| 46 | + break |
| 47 | + |
| 48 | + print(f"Turn {self.turn}: ") |
| 49 | + # Shows the player's potions, cards(in hand), amount of cards in discard and draw pile, and shows the status for you and the enemies. |
| 50 | + view.display_ui(self.player, self.active_enemies) |
| 51 | + print("1-0: Play card, P: Play Potion, M: View Map, D: View Deck, A: View Draw Pile, S: View Discard Pile, X: View Exhaust Pile, E: End Turn, F: View Debuffs and Buffs") |
| 52 | + action = input("> ").lower() |
| 53 | + other_options = { |
| 54 | + "d": lambda: view.view_piles(self.player.deck, end=True), |
| 55 | + "a": lambda: view.view_piles( |
| 56 | + self.player.draw_pile, shuffle=True, end=True |
| 57 | + ), |
| 58 | + "s": lambda: view.view_piles(self.player.discard_pile, end=True), |
| 59 | + "x": lambda: view.view_piles(self.player.exhaust_pile, end=True), |
| 60 | + "p": self.play_potion, |
| 61 | + "f": lambda: ei.full_view(self.player, self.active_enemies), |
| 62 | + "m": lambda: view.view_map(self.game_map), |
| 63 | + } |
| 64 | + if action.isdigit(): |
| 65 | + option = int(action) - 1 |
| 66 | + if option + 1 in range(len(self.player.hand) + 1): |
| 67 | + self.play_new_card(self.player.hand[option]) |
| 68 | + else: |
| 69 | + view.clear() |
| 70 | + continue |
| 71 | + elif action in other_options: |
| 72 | + other_options[action]() |
| 73 | + elif action == "e": |
| 74 | + view.clear() |
| 75 | + break |
| 76 | + else: |
| 77 | + view.clear() |
| 78 | + continue |
| 79 | + sleep(1) |
| 80 | + view.clear() |
| 81 | + if self.player.state == State.ESCAPED: |
| 82 | + self.end_combat(self, escaped=True) |
| 83 | + bus.publish(Message.END_OF_TURN, data=(self.player, self.all_enemies)) |
| 84 | + self.turn += 1 |
| 85 | + |
| 86 | + def end_combat(self, killed_enemies=False, escaped=False, robbed=False): |
| 87 | + if killed_enemies is True: |
| 88 | + potion_roll = random.random() |
| 89 | + ansiprint("<green>Combat finished!</green>") |
| 90 | + self.player.gain_gold(random.randint(10, 20)) |
| 91 | + if (potion_roll < self.player.potion_dropchance): |
| 92 | + gen.claim_potions(True, 1, self.player, potion_catalog.create_all_potions()) |
| 93 | + self.player.potion_dropchance -= 10 |
| 94 | + else: |
| 95 | + self.player.potion_dropchance += 10 |
| 96 | + gen.card_rewards(self.tier, True, self.player, card_catalog.create_all_cards()) |
| 97 | + view.clear() |
| 98 | + elif escaped is True: |
| 99 | + print("Escaped...") |
| 100 | + sleep(0.8) |
| 101 | + print("You recieve nothing.") |
| 102 | + sleep(1.5) |
| 103 | + view.clear() |
| 104 | + elif robbed: |
| 105 | + print("Robbed...") |
| 106 | + sleep(0.8) |
| 107 | + print("You recieve nothing.") |
| 108 | + sleep(1.2) |
| 109 | + view.clear() |
| 110 | + bus.publish(Message.END_OF_COMBAT, (self.tier, self.player)) |
| 111 | + self.player.unsubscribe() |
| 112 | + for enemy in self.all_enemies: |
| 113 | + enemy.unsubscribe() |
| 114 | + |
| 115 | + def on_player_move(self): |
| 116 | + self.update_death_messages() |
| 117 | + self.previous_enemy_states = tuple(enemy.state for enemy in self.all_enemies) |
| 118 | + |
| 119 | + def clean_effects(effects): |
| 120 | + for effect in effects: |
| 121 | + if effect.amount <= 0: |
| 122 | + effect.unsubscribe() |
| 123 | + ansiprint(f"{effect.get_name()} wears off.") |
| 124 | + return [effect for effect in effects if effect.amount >= 1] |
| 125 | + |
| 126 | + for enemy in self.active_enemies: |
| 127 | + enemy.buffs = clean_effects(enemy.buffs) |
| 128 | + enemy.debuffs = clean_effects(enemy.debuffs) |
| 129 | + self.player.buffs, self.player.debuffs = [ |
| 130 | + clean_effects(effects) for effects in (self.player.buffs, self.player.debuffs) |
| 131 | + ] |
| 132 | + |
| 133 | + def create_enemies_from_tier(self) -> list[Enemy]: |
| 134 | + act1_normal_encounters = create_act1_normal_encounters(self.all_enemies) |
| 135 | + act1_elites = create_act1_elites() |
| 136 | + act1_boss = create_act1_boss() |
| 137 | + encounter_types = { |
| 138 | + CombatTier.NORMAL: act1_normal_encounters, |
| 139 | + CombatTier.ELITE: act1_elites, |
| 140 | + CombatTier.BOSS: act1_boss, |
| 141 | + } |
| 142 | + return encounter_types[self.tier][0] |
| 143 | + |
| 144 | + def start_combat(self) -> list[Enemy]: |
| 145 | + self.player.register(bus=bus) |
| 146 | + if not self.all_enemies: |
| 147 | + self.all_enemies = self.create_enemies_from_tier() |
| 148 | + |
| 149 | + for enemy in self.all_enemies: |
| 150 | + enemy.register(bus=bus) |
| 151 | + |
| 152 | + bus.publish(Message.START_OF_COMBAT, (self.tier, self.active_enemies, self.player)) |
| 153 | + self.previous_enemy_states = tuple(enemy.state for enemy in self.all_enemies) |
| 154 | + |
| 155 | + def select_target(self): |
| 156 | + if len(self.active_enemies) == 1: |
| 157 | + return 0 |
| 158 | + else: |
| 159 | + while True: |
| 160 | + try: |
| 161 | + target = int(input("Choose an enemy to target > ")) - 1 |
| 162 | + _ = self.active_enemies[target] |
| 163 | + except (IndexError, ValueError): |
| 164 | + ansiprint(f"\u001b[1A\u001b[100D<red>You have to enter a number between 1 and {len(self.active_enemies)}</red>", end="") |
| 165 | + sleep(1) |
| 166 | + print("\u001b[2K\u001b[100D", end="") |
| 167 | + continue |
| 168 | + return target |
| 169 | + |
| 170 | + def play_new_card(self, card): |
| 171 | + # Prevents the player from using a card that they don't have enough energy for. |
| 172 | + if card.energy_cost > self.player.energy: |
| 173 | + ansiprint("<red>You don't have enough energy to use this card.</red>") |
| 174 | + sleep(1) |
| 175 | + view.clear() |
| 176 | + return |
| 177 | + # Todo: Move to Velvet Choker relic |
| 178 | + if self.player.choker_cards_played == 6: |
| 179 | + ansiprint("You have already played 6 cards this turn!") |
| 180 | + sleep(1) |
| 181 | + view.clear() |
| 182 | + return |
| 183 | + |
| 184 | + if card.target == TargetType.SINGLE: |
| 185 | + target = self.select_target() |
| 186 | + self.player.use_card(card, target=self.active_enemies[target], exhaust=False, pile=self.player.hand, enemies=self.all_enemies) |
| 187 | + else: |
| 188 | + self.player.use_card(card, target=self.active_enemies, exhaust=False, pile=self.player.hand, enemies=self.all_enemies) |
| 189 | + |
| 190 | + def update_death_messages(self): |
| 191 | + current_states = tuple(enemy.state for enemy in self.all_enemies) |
| 192 | + for i in range(0, max(1, len(self.all_enemies) - 1)): |
| 193 | + if self.previous_enemy_states[i] != current_states[i]: |
| 194 | + self.death_messages.append(current_states[i]) |
| 195 | + |
| 196 | + def play_potion(self): |
| 197 | + if len(self.player.potions) == 0: |
| 198 | + ansiprint("<red>You have no potions.</red>") |
| 199 | + return |
| 200 | + chosen_potion = view.list_input("Choose a potion to play", self.player.potions, view.view_potions, lambda potion: potion.playable, "That potion is not playable.") |
| 201 | + potion = self.player.potions.pop(chosen_potion) |
| 202 | + if potion.target == TargetType.YOURSELF: |
| 203 | + potion.apply(self.player) |
| 204 | + elif potion.target == TargetType.SINGLE: |
| 205 | + target = self.select_target() |
| 206 | + potion.apply(self.player, self.active_enemies[target]) |
| 207 | + elif potion.target in (TargetType.AREA, TargetType.ANY): |
| 208 | + potion.apply(self.player, self.active_enemies) |
| 209 | + |
0 commit comments