diff --git a/src/gamestates/battle.py b/src/gamestates/battle.py index b49babd..ae0e514 100644 --- a/src/gamestates/battle.py +++ b/src/gamestates/battle.py @@ -2,18 +2,280 @@ import arcade import assets import controls import text +import math +import random -text = text.TextRenderer(100, 186, 210) +K_NORMAL = 100 +K_DEFUP = 50 +MIN_DMG = 1 +BASE_DELAY = 60000 + +class Combatant: + def __init__(self, name, maxhp, attack, speed, def_phys, def_fire, def_ice, abs_fire, abs_ice): + self.name = name + self.max_atb = BASE_DELAY//speed + self.atb = self.max_atb + self.maxhp = maxhp + self.hp = maxhp + self.attack = attack + self.ko = False + self.stone = False + self.def_up = False + self.def_phys = def_phys + self.def_fire = def_fire + self.def_ice = def_ice + self.abs_fire = abs_fire + self.abs_ice = abs_ice + +# POTENTIAL damage before defense effects +class Damage: + def __init__(self, phys, fire, ice, healing): + self.phys= phys + self.fire= fire + self.ice= ice + self.healing = healing + +# 0 = keep, 1 = inflict, 2 = remove +class StatusChange: + def __init__(self, ko, stone, def_up): + self.ko = ko + self.stone = stone + self.def_up = def_up + +# where = reference to the target, who = reference to the inflictor +class Action: + def __init__(self, name, who, where, damage, status): + self.name = name + self.who = who + self.where = where + self.damage = damage + self.status = status + + def __str__(self): + return ( + f"[{self.name}] {self.who.name} -> {self.where.name} " + f"(HP influence: ({self.damage.phys},{self.damage.fire},{self.damage.ice},{self.damage.healing}), Status: {self.status.ko}-{self.status.stone}-{self.status.def_up})" + ) + + def __repr__(self): + return self.__str__() + + def execute(self): + target = self.where + if self.who.ko or self.who.stone: + print(f" -> {self.who.name} cannot move!") + return + + self.who.atb = self.who.max_atb + + if target.ko and self.status.ko != 2: + print(f" -> {target.name} is KO'd, move cancelled!") + return + + K = K_DEFUP if target.def_up else K_NORMAL + + delta_hp = self.damage.healing + + if self.damage.phys: + delta_hp += -max(MIN_DMG, self.damage.phys * (K / (K + target.def_phys))) + + if self.damage.fire: + delta_hp += self.damage.fire if target.abs_fire else -max(MIN_DMG, self.damage.fire * (K / (K + target.def_fire))) + + if self.damage.ice: + delta_hp += self.damage.ice if target.abs_ice else -max(MIN_DMG, self.damage.ice * (K / (K + target.def_ice ))) + + if self.status.ko == 1: + delta_hp = -target.hp + elif self.status.ko == 2 and target.ko: + target.ko = False + print(f" -> {target.name}'s consciousness returned!") + + delta_hp = round(delta_hp) + + if delta_hp > 0: + print(f" -> {target.name} got healed for {delta_hp} HP!") + elif delta_hp < 0: + print(f" -> {target.name} suffered {-delta_hp} damage!") + + target.hp = max(0, min(target.maxhp, target.hp + delta_hp)) + + if self.status.stone == 1 and not target.stone: + target.stone = True + print(f" -> {target.name}'s body petrified!") + elif self.status.stone == 2 and target.stone: + target.stone = False + print(f" -> {target.name}'s body softened back up!") + + if self.status.def_up == 1 and not target.def_up: + target.def_up = True + print(f" -> {target.name}'s defense was boosted!") + elif self.status.def_up == 2 and target.def_up: + target.def_up = False + print(f" -> {target.name}'s defense returned to normal!") + + if target.hp == 0: + target.ko = True + target.stone = False + target.def_up = False + print(f" -> {target.name} got hurt and collapsed!") +def move_attack(user, target): + return Action("Attack", user, target, Damage(user.attack, 0, 0, 0), StatusChange(0, 0, 0)) + +def move_fire(user, target): + return Action("Fira", user, target, Damage(0, 45, 0, 0), StatusChange(0, 0, 0)) + +def move_ice(user, target): + return Action("Blizzara", user, target, Damage(0, 0, 45, 0), StatusChange(0, 0, 0)) + +def move_cure(user, target): + return Action("Cure", user, target, Damage(0, 0, 0, 42), StatusChange(0, 0, 0)) + +def move_defend(user, target): + return Action("Defend", user, target, Damage(0, 0, 0, 0), StatusChange(0, 0, 1)) + +def move_break(user, target): + return Action("Break", user, target, Damage(0, 0, 0, 0), StatusChange(0, 1, 0)) + +def move_death(user, target): + return Action("Death", user, target, Damage(0, 0, 0, 0), StatusChange(1, 0, 0)) + +def move_phoenix(user, target): + return Action("Phoenix Down", user, target, Damage(0, 0, 0, 30), StatusChange(2, 0, 0)) + +def move_softenup(user, target): + return Action("Esuna (remove stoneskin)", user, target, Damage(0, 0, 0, 0), StatusChange(2, 0, 0)) + +enemies = [ + # name hp atk spd dphys dfire dice absF absI + Combatant("Six-Seven kid", 120, 30, 80, 20, 0, 50, False, False), + Combatant("Ice Sprite", 60, 15, 150, 10, 0, 200, False, True ), # ice-immune, fast, fragile + Combatant("Magma Brute", 200, 40, 40, 60, 999, -50, True, False), # absorbs fire, weak to ice, slow tank +] + +party_members = [ + # name hp atk spd dphys dfire dice absF absI + Combatant("Terra", 100, 35, 100, 30, 30, 30, False, False), + Combatant("Locke", 80, 45, 130, 15, 10, 10, False, False), # glass cannon, fast, low def + Combatant("Celes", 110, 25, 90, 50, 40, 40, False, False), # tanky + Combatant("Cyan", 130, 38, 70, 40, 20, 20, False, False), +] + +party_text = text.TextRenderer(3, 206, 210) +ene_text = text.TextRenderer(123, 206, 210) +menu_text = text.TextRenderer(3, 36, 210) +action_queue = [] battle_music = None +menu_open = 0 +menu_queue = [] + +MOVES = { + "attack": ("Physical attack", move_attack, True), + "fire": ("Fira (fire magic)", move_fire, True), + "ice": ("Blizzara (ice magic)", move_ice, True), + "cure": ("Cure (heal ally)", move_cure, False), + "defend": ("Defend (self def_up)", move_defend, None), # self-only, forced + "break": ("Break (inflict stone)", move_break, True), + "death": ("Death (instant KO)", move_death, True), + "phoenix": ("Phoenix Down (revive ally)",move_phoenix, False), + "unstone": ("Esuna (cure stone)", move_softenup,False), +} + +def enemy_turn(e): + living = [p for p in party_members if not p.ko] + if not living: + return None + + # weighted move pool -- enemies get the same toolkit as the party + offensive = ["attack", "fire", "ice", "death"] + support = ["break"] + self_only = ["defend"] + + # if an ally is KO'd, occasionally try to revive/cure instead of attacking + ko_allies = [a for a in enemies if a.ko and a is not e] + hurt_allies = [a for a in enemies if not a.ko and a.hp < a.maxhp] + + roll = random.random() + + if ko_allies and roll < 0.15: + act = "phoenix" + target = random.choice(ko_allies) + elif hurt_allies and roll < 0.30: + act = "cure" + target = random.choice(hurt_allies) + elif not e.def_up and roll < 0.40: + act = "defend" + target = e + elif roll < 0.55: + act = "break" + target = random.choice(living) + elif roll < 0.65: + act = "death" + target = random.choice(living) + else: + act = random.choice(["attack", "fire", "ice"]) + target = random.choice(living) + + _, builder, _ = MOVES[act] + print(f" ({e.name} chooses {act} -> {target.name})") + return builder(e, target) + + def switch(game): global battle_music battle_music = assets.sounds["mus_fnafsux"].play(loops=True) - text.reset("ASDF") - text.type_all() def update(game, dt): - pass + global menu_open + + for e in enemies: + if e.ko or e.stone: + continue + e.atb -= 1 + if e.atb <= 0: + act = enemy_turn(e) + if act: + action_queue.append(act) + e.atb = math.inf + + for p in party_members: + if p.ko or p.stone: + continue + p.atb -= 1 + if p.atb <= 0: + print(f"{p.name} is ready to act!") + menu_queue.append(p) + p.atb = math.inf + + if len(action_queue) != 0: + action = action_queue.pop(0) + print(f"Executing: {action}") + action.execute() + + # Szövegek Frissítése + partytext = "" + enetext = "" + + for m in party_members: + partytext += f"=={m.name}==\nHP: {m.hp}\n\n" + + for e in enemies: + enetext += f"=={e.name}==\nHP: {e.hp}\n\n" + + if len(menu_queue) != 0 and menu_open == 0: + whose = menu_queue.pop(0) + menu_open = whose + menu_text.reset(f"{whose.name}'s Menu") + menu_text.type_all() + + party_text.reset(partytext) + ene_text.reset(enetext) + party_text.type_all() + ene_text.type_all() def draw(game): - text.render() + party_text.render() + ene_text.render() + menu_text.render() +