battle.py

"""Battle engine: handles all combat calculations and the battle loop."""

from __future__ import annotations

import random

import math

from typing import List, Optional, Tuple, Dict

from data_types import *

from character import Character

from monster_unit import MonsterUnit

from monsters_db import MonsterTemplate

from skills_db import SKILL_DB

from items_db import ITEM_DB

def _clear():

 import os; os.system('clear' if os.name == 'posix' else 'cls')

def calculate_physical_damage(attacker_patk: int, target_pdef: int,

 power: float = 1.0, crit: bool = False,

 element_bonus: float = 1.0,

 weakness_bonus: float = 1.0,

 break_bonus: float = 1.0) -> int:

 base = max(1, attacker_patk - target_pdef // 2)

 dmg = int(base * power * element_bonus * weakness_bonus * break_bonus)

 if crit:

 dmg = int(dmg * 1.5)

 dmg = max(1, int(dmg * random.uniform(0.9, 1.1)))

 return dmg

def calculate_magical_damage(attacker_matk: int, target_mdef: int,

 power: float = 1.0, crit: bool = False,

 element_bonus: float = 1.0,

 weakness_bonus: float = 1.0,

 break_bonus: float = 1.0) -> int:

 base = max(1, attacker_matk - target_mdef // 3)

 dmg = int(base * power * element_bonus * weakness_bonus * break_bonus)

 if crit:

 dmg = int(dmg * 1.5)

 dmg = max(1, int(dmg * random.uniform(0.9, 1.1)))

 return dmg

def calculate_heal(healer_mdef: int, power: float = 1.0) -> int:

 return max(1, int(healer_mdef * 2.5 * power * random.uniform(0.9, 1.1)))

def check_hit(attacker_acc: float, target_eva: float) -> bool:

 hit_chance = min(0.99, max(0.01, attacker_acc - target_eva))

 return random.random() < hit_chance

def check_crit(crit_rate: float) -> bool:

 return random.random() < crit_rate

class Battle:

 def __init__(self, party: List[Character], monsters: List[MonsterUnit]):

 self.party = party

 self.monsters = monsters

 self.turn = 0

 self.battle_log: List[str] = []

 self._defending: set = set() # character names defending this round

 # ── Display helpers ───────────────────────────────────────────────────

 def display_battle_state(self):

 print("\n" + "═"*65)

 print(" BATTLE STATUS")

 print("═"*65)

 print(" ENEMIES:")

 for i, mu in enumerate(self.monsters):

 if mu.is_dead:

 print(f" [{i+1}] {mu.instance_name} - ☠ DEFEATED")

 else:

 broken = " ⚡BREAK!" if mu.is_broken else ""

 print(f" [{i+1}] {mu.instance_name} Lv{mu.template.level}{broken}")

 print(f" HP: {mu.hp_bar()}")

 print(f" Shield: {mu.shield_bar()}{mu.status_str()}")

 print()

 print(" ALLIES:")

 for i, ch in enumerate(self.party):

 if ch.is_dead:

 print(f" [{i+1}] {ch.name} [DEAD]")

 elif ch.is_ko:

 print(f" [{i+1}] {ch.name} [KO - {3 - ch.ko_turns} turns left]")

 else:

 print(f" [{i+1}] {ch.name} Lv{ch.level} {ch.job.value}{ch.status_str()}")

 print(f" HP: {ch.hp_bar()} | MP: {ch.mp_bar()}")

 print("═"*65)

 def display_turn_order(self, order: List):

 units = []

 for u in order:

 if isinstance(u, Character):

 units.append(f"{u.name}({u.spd})")

 elif isinstance(u, MonsterUnit):

 units.append(f"{u.instance_name}({u.spd})")

 print(f"\n Turn Order: {' → '.join(units)}")

 def _log(self, msg: str):

 self.battle_log.append(msg)

 print(msg)

 # ── Turn order ────────────────────────────────────────────────────────

 def get_turn_order(self) -> List:

 units = []

 for ch in self.party:

 if not ch.is_ko and not ch.is_dead:

 units.append(ch)

 for mu in self.monsters:

 if not mu.is_dead:

 units.append(mu)

 # Sort by speed descending; defenders get priority

 units.sort(key=lambda u: (

 1 if (isinstance(u, Character) and u.name in self._defending) else 0,

 u.spd

 ), reverse=True)

 return units

 # ── Main battle loop ──────────────────────────────────────────────────

 def run(self) -> bool:

 """Returns True if party wins."""

 print("\n" + "★"*65)

 print(" ★ BATTLE START! ★")

 print("★"*65)

 input(" [Press Enter]")

 while True:

 self.turn += 1

 self._defending.clear()

 print(f"\n{'─'*65}")

 print(f" ── TURN {self.turn} ──")

 self.display_battle_state()

 order = self.get_turn_order()

 self.display_turn_order(order)

 for unit in order:

 if self._check_battle_end():

 break

 if isinstance(unit, Character):

 if unit.is_ko or unit.is_dead:

 continue

 self._player_turn(unit)

 elif isinstance(unit, MonsterUnit):

 if unit.is_dead:

 continue

 self._monster_turn(unit)

 # End of round: status ticks, KO timers, break recovery

 self._end_of_round()

 if self._check_battle_end():

 break

 return self._is_victory()

 def _check_battle_end(self) -> bool:

 all_monsters_dead = all(mu.is_dead for mu in self.monsters)

 all_party_out = all(ch.is_ko or ch.is_dead for ch in self.party)

 return all_monsters_dead or all_party_out

 def _is_victory(self) -> bool:

 return all(mu.is_dead for mu in self.monsters)

 # ── Player turn ───────────────────────────────────────────────────────

 def _player_turn(self, ch: Character):

 print(f"\n ── {ch.name}'s Turn ({ch.job.value}) ──")

 if ch.is_incapacitated():

 incap_se = [se for se in ch.status_effects

 if se.effect_type in {StatusEffectType.SLEEP, StatusEffectType.STUN,

 StatusEffectType.FREEZE, StatusEffectType.PARALYZE,

 StatusEffectType.PETRIFY}]

 if incap_se:

 self._log(f" {ch.name} is {incap_se[0].effect_type.value}! Can't act.")

 # Wake up chance for sleep

 if incap_se[0].effect_type == StatusEffectType.SLEEP:

 if random.random() < 0.25:

 ch.remove_status(StatusEffectType.SLEEP)

 self._log(f" {ch.name} woke up!")

 return

 while True:

 print(f"\n Actions: [1] Attack [2] Skill [3] Item [4] Defend")

 choice = input(" > ").strip()

 if choice == "1":

 target = self._pick_enemy_target()

 if target:

 self._do_basic_attack(ch, target)

 break

 elif choice == "2":

 if not ch.can_use_skills():

 print(" Skills are sealed!")

 continue

 skill_id = self._pick_skill(ch)

 if skill_id is None:

 continue

 skill = SKILL_DB[skill_id]

 if not ch.can_use_magic() and skill.skill_type == SkillType.MAGICAL:

 print(" Cannot use magic (Silenced)!")

 continue

 if ch.current_mp < skill.mp_cost:

 print(f" Not enough MP! (Need {skill.mp_cost}, have {ch.current_mp})")

 continue

 self._do_skill(ch, skill)

 break

 elif choice == "3":

 if not ch.can_use_items():

 print(" Items are sealed!")

 continue

 used = self._use_item_menu(ch)

 if used:

 break

 elif choice == "4":

 self._do_defend(ch)

 break

 else:

 print(" Invalid choice.")

 def _pick_enemy_target(self) -> Optional[MonsterUnit]:

 alive = [mu for mu in self.monsters if not mu.is_dead]

 if not alive:

 return None

 if len(alive) == 1:

 return alive[0]

 print(" Choose target:")

 for i, mu in enumerate(alive):

 broken = " ⚡BREAK" if mu.is_broken else ""

 print(f" [{i+1}] {mu.instance_name} HP:{mu.current_hp}/{mu.max_hp}{broken}")

 while True:

 try:

 idx = int(input(" > ")) - 1

 if 0 <= idx < len(alive):

 return alive[idx]

 except ValueError:

 pass

 print(" Invalid choice.")

 def _pick_ally_target(self, include_ko=False) -> Optional[Character]:

 if include_ko:

 valid = [ch for ch in self.party if not ch.is_dead]

 else:

 valid = [ch for ch in self.party if not ch.is_ko and not ch.is_dead]

 if not valid:

 return None

 if len(valid) == 1:

 return valid[0]

 print(" Choose ally:")

 for i, ch in enumerate(valid):

 status = "KO" if ch.is_ko else f"HP:{ch.current_hp}/{ch.max_hp}"

 print(f" [{i+1}] {ch.name} ({status})")

 while True:

 try:

 idx = int(input(" > ")) - 1

 if 0 <= idx < len(valid):

 return valid[idx]

 except ValueError:

 pass

 print(" Invalid choice.")

 def _pick_skill(self, ch: Character) -> Optional[int]:

 if not ch.unlocked_skills:

 print(" No skills available!")

 return None

 print(" Choose skill (0=back):")

 for i, sid in enumerate(ch.unlocked_skills):

 if sid in SKILL_DB:

 sk = SKILL_DB[sid]

 print(f" [{i+1}] {sk.name} "

 f"[{sk.tier.value}|{sk.skill_type.value}|{sk.element.value}] "

 f"MP:{sk.mp_cost} PWR:{sk.power} ACC:{int(sk.accuracy*100)}%")

 while True:

 raw = input(" > ").strip()

 if raw == "0":

 return None

 try:

 idx = int(raw) - 1

 if 0 <= idx < len(ch.unlocked_skills):

 return ch.unlocked_skills[idx]

 except ValueError:

 pass

 print(" Invalid choice.")

 def _use_item_menu(self, ch: Character) -> bool:

 """Returns True if an item was successfully used."""

 from game_state import GameState

 # Items accessed via global inventory - we'll use a simplified approach

 # The GameState will be passed in during actual game run

 print(" (Item system: handled by game state)")

 return False # placeholder - overridden in actual game

 def _do_basic_attack(self, ch: Character, target: MonsterUnit):

 # Hit check

 hit = check_hit(ch.acc, target.eva)

 if not hit:

 self._log(f" {ch.name} attacks {target.instance_name}... MISS!")

 return

 is_crit = check_crit(ch.crit)

 # Weakness check

 is_weak = target.hit_weakness(ch.weapon, ch.element)

 weakness_bonus = 1.3 if is_weak else 1.0

 break_bonus = 1.5 if target.is_broken else 1.0

 dmg = calculate_physical_damage(

 ch.patk, target.pdef, power=1.0,

 crit=is_crit, weakness_bonus=weakness_bonus, break_bonus=break_bonus

 )

 # Defending reduction

 if ch.name in self._defending:

 dmg = int(dmg * 0.7)

 actual = target.take_damage(dmg)

 crit_str = " CRITICAL!" if is_crit else ""

 weak_str = " 【WEAKNESS】" if is_weak else ""

 self._log(f" {ch.name} attacks {target.instance_name}!{crit_str}{weak_str}")

 self._log(f" Dealt {actual} damage!")

 # Shield break

 if is_weak:

 broke = target.reduce_shield(1)

 if broke:

 self._log(f" ⚡ {target.instance_name} is BROKEN! All defenses down for 1 turn!")

 else:

 self._log(f" Shield: {target.shield_bar()} ({target.shield_points} remaining)")

 # Counter

 if target.has_status(StatusEffectType.COUNTER) and not target.is_dead:

 cdmg = calculate_physical_damage(target.patk, ch.pdef, power=0.5)

 ch.take_damage(cdmg)

 self._log(f" {target.instance_name} COUNTER! {ch.name} takes {cdmg} damage!")

 def _do_skill(self, ch: Character, skill: Skill):

 ch.current_mp -= skill.mp_cost

 # Determine targets

 targets_enemies = []

 targets_allies = []

 if skill.target == SkillTarget.SINGLE_ENEMY:

 t = self._pick_enemy_target()

 if t: targets_enemies = [t]

 elif skill.target == SkillTarget.ALL_ENEMIES:

 targets_enemies = [m for m in self.monsters if not m.is_dead]

 elif skill.target == SkillTarget.RANDOM_ENEMY:

 alive = [m for m in self.monsters if not m.is_dead]

 if alive:

 targets_enemies = random.choices(alive, k=skill.hits)

 elif skill.target == SkillTarget.SINGLE_ALLY:

 t = self._pick_ally_target(include_ko=skill.skill_type == SkillType.HEAL)

 if t: targets_allies = [t]

 elif skill.target == SkillTarget.ALL_ALLIES:

 targets_allies = [c for c in self.party if not c.is_dead]

 elif skill.target == SkillTarget.SELF:

 targets_allies = [ch]

 elem_name = f"[{skill.element.value}]" if skill.element != Element.NONE else ""

 self._log(f"\n ✦ {ch.name} uses {skill.name}!{elem_name}")

 # Heal skills

 if skill.skill_type == SkillType.HEAL:

 for target in targets_allies:

 if skill.mp_cost == 20 and skill.id == 46: # Resurrection special case

 if target.is_ko:

 target.revive(100)

 self._log(f" {target.name} is REVIVED with full HP!")

 else:

 self._log(f" {target.name} is already standing.")

 else:

 heal_amt = calculate_heal(ch.mdef, skill.power)

 actual = target.heal(heal_amt)

 self._log(f" {target.name} recovers {actual} HP!")

 return

 # Buff skills

 if skill.skill_type == SkillType.BUFF:

 if skill.effect:

 for target in targets_allies:

 se = StatusEffect(skill.effect.status, skill.effect.duration, skill.effect.stat_modifier)

 target.add_status(se)

 self._log(f" {target.name} gains {skill.effect.status.value}!")

 return

 # Debuff skills

 if skill.skill_type == SkillType.DEBUFF:

 for target in targets_enemies:

 if skill.effect and skill.effect.status:

 chance = skill.effect.chance if skill.effect.chance > 0 else 0.8

 if random.random() < chance:

 se = StatusEffect(skill.effect.status, skill.effect.duration)

 target.add_status(se)

 self._log(f" {target.instance_name} is afflicted with {skill.effect.status.value}!")

 else:

 self._log(f" {target.instance_name} resists!")

 return

 # Damage skills (physical / magical / special)

 num_hits = skill.hits if skill.target != SkillTarget.RANDOM_ENEMY else 1

 raw_targets = targets_enemies

 if skill.target == SkillTarget.RANDOM_ENEMY:

 alive = [m for m in self.monsters if not m.is_dead]

 raw_targets = []

 for _ in range(skill.hits):

 if alive: raw_targets.append(random.choice(alive))

 for target in raw_targets:

 for hit_n in range(num_hits if skill.target != SkillTarget.RANDOM_ENEMY else 1):

 # Hit check

 if not check_hit(ch.acc * skill.accuracy, target.eva):

 self._log(f" Hit {hit_n+1}: MISS on {target.instance_name}!")

 continue

 is_crit = check_crit(ch.crit)

 is_weak = target.hit_weakness(

 ch.weapon if skill.skill_type == SkillType.PHYSICAL else None,

 skill.element if skill.element != Element.NONE else ch.element

 )

 weakness_bonus = 1.3 if is_weak else 1.0

 break_bonus = 1.5 if target.is_broken else 1.0

 # Weakness mark doubles weakness

 if target.has_status(StatusEffectType.WEAKNESS_MARK) and is_weak:

 weakness_bonus *= 1.3

 if skill.skill_type == SkillType.PHYSICAL:

 dmg = calculate_physical_damage(

 ch.patk, target.pdef, skill.power,

 is_crit, weakness_bonus=weakness_bonus, break_bonus=break_bonus)

 else:

 # Magic boost

 matk = ch.matk

 if ch.has_status(StatusEffectType.MAGIC_BOOST):

 matk = int(matk * 1.4)

 # Reflect check

 if target.has_status(StatusEffectType.REFLECT):

 self._log(f" {target.instance_name} REFLECTS the spell!")

 friendly = [c for c in self.party if not c.is_ko and not c.is_dead]

 if friendly:

 rf_target = random.choice(friendly)

 rdmg = calculate_magical_damage(matk, rf_target.mdef, skill.power)

 rf_target.take_damage(rdmg)

 self._log(f" Reflected! {rf_target.name} takes {rdmg} damage!")

 continue

 dmg = calculate_magical_damage(

 matk, target.mdef, skill.power,

 is_crit, weakness_bonus=weakness_bonus, break_bonus=break_bonus)

 actual = target.take_damage(dmg)

 crit_str = " CRITICAL!" if is_crit else ""

 weak_str = " 【WEAKNESS】" if is_weak else ""

 self._log(f" {target.instance_name} takes {actual} damage!{crit_str}{weak_str}")

 # Shield damage for weakness hits

 if is_weak:

 broke = target.reduce_shield(1)

 if broke:

 self._log(f" ⚡ {target.instance_name} is BROKEN!")

 else:

 self._log(f" Shield: {target.shield_bar()}")

 # Apply effect

 if skill.effect and skill.effect.status and not target.is_dead:

 chance = skill.effect.chance if skill.effect.chance > 0 else 0.5

 if random.random() < chance:

 se = StatusEffect(skill.effect.status, skill.effect.duration,

 skill.effect.stat_modifier)

 target.add_status(se)

 self._log(f" {target.instance_name} afflicted with {skill.effect.status.value}!")

 def _do_defend(self, ch: Character):

 self._defending.add(ch.name)

 ch.add_status(StatusEffect(StatusEffectType.DEFENDING, 1))

 self._log(f" {ch.name} takes a defensive stance! (DMG -30% next turn, speed priority)")

 # ── Monster turn ──────────────────────────────────────────────────────

 def _monster_turn(self, mu: MonsterUnit):

 if mu.is_incapacitated():

 self._log(f"\n {mu.instance_name} is incapacitated and cannot act!")

 return

 alive_party = [ch for ch in self.party if not ch.is_ko and not ch.is_dead]

 if not alive_party:

 return

 action = mu.choose_action(alive_party)

 target = random.choice(alive_party)

 if action["action"] == "attack":

 self._monster_basic_attack(mu, target)

 elif action["action"] == "skill":

 skill_id = action["skill_id"]

 if skill_id in SKILL_DB:

 skill = SKILL_DB[skill_id]

 self._monster_skill(mu, skill, alive_party)

 else:

 self._monster_basic_attack(mu, target)

 elif action["action"] == "defend":

 self._log(f" {mu.instance_name} braces for impact!")

 def _monster_basic_attack(self, mu: MonsterUnit, target: Character):

 hit = check_hit(mu.acc, target.eva)

 if not hit:

 self._log(f"\n {mu.instance_name} attacks {target.name}... MISS!")

 return

 is_crit = check_crit(mu.template.base_stats.crit)

 # Defend reduction

 defending = target.has_status(StatusEffectType.DEFENDING)

 dmg = calculate_physical_damage(

 mu.patk, target.pdef, crit=is_crit)

 if defending:

 dmg = int(dmg * 0.7)

 # Confusion: might attack ally

 if mu.has_status(StatusEffectType.CONFUSION):

 alive_enemies = [m for m in self.monsters if not m.is_dead and m != mu]

 if alive_enemies and random.random() < 0.5:

 friendly_target = random.choice(alive_enemies)

 actual = friendly_target.take_damage(dmg)

 self._log(f"\n {mu.instance_name} is confused and attacks {friendly_target.instance_name}! ({actual} dmg)")

 return

 crit_str = " CRITICAL!" if is_crit else ""

 self._log(f"\n {mu.instance_name} attacks {target.name}!{crit_str}")

 target.take_damage(dmg)

 self._log(f" {target.name} takes {dmg} damage!")

 # Counter

 if target.has_status(StatusEffectType.COUNTER) and not target.is_ko:

 cdmg = calculate_physical_damage(target.patk, mu.pdef, power=0.5)

 mu.take_damage(cdmg)

 self._log(f" {target.name} COUNTER! {mu.instance_name} takes {cdmg}!")

 def _monster_skill(self, mu: MonsterUnit, skill: Skill, alive_party: List[Character]):

 self._log(f"\n {mu.instance_name} uses {skill.name}!")

 # Silence check for magic

 if skill.skill_type == SkillType.MAGICAL and mu.has_status(StatusEffectType.SILENCE):

 self._log(f" {mu.instance_name} is silenced!")

 return

 target = random.choice(alive_party)

 if skill.skill_type in (SkillType.PHYSICAL, SkillType.MAGICAL, SkillType.SPECIAL):

 for _ in range(skill.hits):

 if target.is_ko: break

 hit = check_hit(mu.acc * skill.accuracy, target.eva)

 if not hit:

 self._log(f" MISS on {target.name}!")

 continue

 is_crit = check_crit(mu.template.base_stats.crit)

 defending = target.has_status(StatusEffectType.DEFENDING)

 if skill.skill_type == SkillType.PHYSICAL:

 dmg = calculate_physical_damage(mu.patk, target.pdef, skill.power, is_crit)

 else:

 # Reflect check

 if target.has_status(StatusEffectType.REFLECT):

 self._log(f" {target.name} REFLECTS the spell!")

 rdmg = calculate_magical_damage(mu.matk, mu.mdef, skill.power)

 mu.take_damage(rdmg)

 self._log(f" Reflected! {mu.instance_name} takes {rdmg} damage!")

 continue

 dmg = calculate_magical_damage(mu.matk, target.mdef, skill.power, is_crit)

 if defending:

 dmg = int(dmg * 0.7)

 crit_str = " CRITICAL!" if is_crit else ""

 target.take_damage(dmg)

 self._log(f" {target.name} takes {dmg} damage!{crit_str}")

 if skill.effect and skill.effect.status and not target.is_ko:

 chance = skill.effect.chance if skill.effect.chance > 0 else 0.4

 if random.random() < chance:

 se = StatusEffect(skill.effect.status, skill.effect.duration,

 skill.effect.stat_modifier)

 target.add_status(se)

 self._log(f" {target.name} afflicted with {skill.effect.status.value}!")

 elif skill.skill_type == SkillType.HEAL:

 heal_amt = calculate_heal(mu.template.base_stats.mdef, skill.power)

 # Heal self or an alive ally monster

 alive_m = [m for m in [mu] if not m.is_dead]

 for m in alive_m:

 old = m.current_hp

 m.current_hp = min(m.max_hp, m.current_hp + heal_amt)

 self._log(f" {m.instance_name} recovers {m.current_hp - old} HP!")

 # ── End of round processing ───────────────────────────────────────────

 def _end_of_round(self):

 print(f"\n ── End of Turn {self.turn} ──")

 # Status effect ticks for party

 for ch in self.party:

 if not ch.is_ko and not ch.is_dead:

 msgs = ch.tick_status_effects()

 msgs += ch.tick_buffs()

 for m in msgs: self._log(m)

 # KO timer management

 for ch in self.party:

 if ch.is_ko and not ch.is_dead:

 ch.ko_turns += 1

 if ch.ko_turns >= 3:

 ch.is_dead = True

 self._log(f" 💀 {ch.name} has DIED! (Too long KO'd)")

 # Status ticks for monsters

 for mu in self.monsters:

 if not mu.is_dead:

 msgs = mu.tick_status_effects()

 for m in msgs: self._log(m)

 mu.tick_break()

 # Remove Defending status

 for ch in self.party:

 ch.remove_status(StatusEffectType.DEFENDING)

 if not self._check_battle_end():

 input("\n [Press Enter to continue]")

 def calculate_exp_reward(self) -> int:

 total = sum(mu.template.exp_reward for mu in self.monsters)

 return total

 def use_item_in_battle(self, ch: Character, item_id: int,

 inventory: Dict[int,int]) -> bool:

 """Use an item from inventory in battle. Returns True if used."""

 if item_id not in inventory or inventory[item_id] <= 0:

 print(" You don't have that item!")

 return False

 item = ITEM_DB[item_id]

 inventory[item_id] -= 1

 if inventory[item_id] <= 0:

 del inventory[item_id]

 self._log(f" {ch.name} uses {item.name}!")

 if item.item_type == ItemType.RECOVERY:

 # Determine target

 if item.target in (SkillTarget.SINGLE_ALLY, SkillTarget.SELF):

 valid = [c for c in self.party if not c.is_dead]

 if item.target == SkillTarget.SELF:

 valid = [ch]

 if not valid: return True

 target = valid[0] if len(valid)==1 else self._pick_ally_target()

 if not target: return True

 targets = [target]

 else:

 targets = [c for c in self.party if not c.is_dead]

 for t in targets:

 if item.effect_value == -100:

 t.current_hp = t.max_hp

 t.current_mp = t.max_mp

 self._log(f" {t.name} fully restored!")

 elif item.effect_value < 0:

 pct = abs(item.effect_value)

 amt = int(t.max_hp * pct / 100)

 actual = t.heal(amt)

 self._log(f" {t.name} recovers {actual} HP!")

 elif item.id in (6,7,8,9,12,20): # MP items

 mp_gain = min(item.effect_value, t.max_mp - t.current_mp)

 t.current_mp += mp_gain

 self._log(f" {t.name} recovers {mp_gain} MP!")

 else:

 actual = t.heal(item.effect_value)

 self._log(f" {t.name} recovers {actual} HP!")

 elif item.item_type == ItemType.REVIVAL:

 valid = [c for c in self.party if c.is_ko and not c.is_dead]

 if not valid:

 self._log(" No KO'd allies to revive!")

 return True

 target = valid[0] if len(valid)==1 else self._pick_ally_target(include_ko=True)

 if not target or not target.is_ko: return True

 if item.target == SkillTarget.ALL_ALLIES:

 for t in valid:

 t.revive(item.effect_value)

 self._log(f" {t.name} revived with {item.effect_value}% HP!")

 else:

 target.revive(item.effect_value)

 self._log(f" {target.name} revived with {item.effect_value}% HP!")

 elif item.item_type == ItemType.STATUS_CURE:

 valid = [c for c in self.party if not c.is_dead]

 if item.target == SkillTarget.ALL_ALLIES:

 targets = valid

 else:

 t = self._pick_ally_target()

 targets = [t] if t else []

 for t in targets:

 if item.status_cure:

 for stype in item.status_cure:

 t.remove_status(stype)

 self._log(f" {t.name} cleansed of ailments!")

 elif item.item_type == ItemType.BUFF:

 valid = [c for c in self.party if not c.is_dead and not c.is_ko]

 if item.target == SkillTarget.ALL_ALLIES:

 targets = valid

 elif item.target == SkillTarget.SELF:

 targets = [ch]

 else:

 t = self._pick_ally_target()

 targets = [t] if t else []

 for t in targets:

 if item.stat_buff:

 for stat, mult in item.stat_buff.items():

 t.add_buff(stat, mult, item.buff_duration)

 self._log(f" {t.name} gains buffs!")

 elif item.item_type == ItemType.OFFENSIVE:

 alive_enemies = [m for m in self.monsters if not m.is_dead]

 if item.target in (SkillTarget.SINGLE_ENEMY,):

 t = self._pick_enemy_target()

 if not t: return True

 targets = [t]

 else:

 targets = alive_enemies

 for t in targets:

 is_weak = item.element and t.hit_weakness(None, item.element)

 w_bonus = 1.3 if is_weak else 1.0

 b_bonus = 1.5 if t.is_broken else 1.0

 dmg = int(item.effect_value * w_bonus * b_bonus)

 actual = t.take_damage(dmg)

 w_str = " 【WEAKNESS】" if is_weak else ""

 self._log(f" {t.instance_name} takes {actual} damage!{w_str}")

 if is_weak:

 broke = t.reduce_shield(1)

 if broke:

 self._log(f" ⚡ {t.instance_name} is BROKEN!")

 elif item.item_type in (ItemType.ADVANCED, ItemType.REVIVAL):

 # Handle advanced items

 if item.effect_value == -100 or item.effect_value == 9999:

 valid = [c for c in self.party if not c.is_dead]

 for t in valid:

 if item.target == SkillTarget.ALL_ALLIES:

 pass

 elif item.target == SkillTarget.SINGLE_ALLY:

 t = self._pick_ally_target()

 valid = [t] if t else []

 break

 for t in valid:

 if item.effect_value == 9999:

 actual = t.heal(9999)

 self._log(f" {t.name} recovers {actual} HP!")

 else:

 t.current_hp = t.max_hp

 t.current_mp = t.max_mp

 self._log(f" {t.name} fully restored!")

 if item.stat_buff:

 for stat, mult in item.stat_buff.items():

 t.add_buff(stat, mult, item.buff_duration)

 return True

 

  