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