from __future__ import annotations
import math
from typing import TYPE_CHECKING
from ..systems.combat_resolver import CombatResolver, DamageType
from .base import Action, ActionResult, Event, ResourceCost
if TYPE_CHECKING:
from ..core.world import World
from ..entities.base import Entity
[docs]
class CombatAction(Action):
def __init__(
self,
actor_id: int,
target_id: int,
damage_type: str = DamageType.PHYSICAL,
base_range: float = 1.0,
):
super().__init__(actor_id)
self.target_id = target_id
self.damage_type = damage_type
self.base_range = base_range
self.combat_resolver = CombatResolver()
[docs]
def can_execute(self, actor: Entity, world: World) -> bool:
target = world.entities.get(self.target_id)
if not target:
return False
if not target.stats.is_alive():
return False
distance = actor.distance_to(target)
# Add small epsilon for floating-point tolerance
if distance > self.base_range + 0.01:
return False
return self.get_cost().can_afford(actor)
[docs]
def execute(self, actor: Entity, world: World) -> ActionResult:
target = world.entities.get(self.target_id)
if not target:
return ActionResult.failure("Target not found")
if not self.can_execute(actor, world):
return ActionResult.failure("Cannot execute attack")
cost = self.get_cost()
if not cost.consume(actor):
return ActionResult.failure("Insufficient resources for attack")
weapon_damage, critical_chance, critical_multiplier, accuracy = (
self._get_weapon_stats(actor)
)
combat_result = self.combat_resolver.resolve_attack(
attacker=actor,
defender=target,
weapon_damage=weapon_damage,
damage_type=self.damage_type,
critical_chance=critical_chance,
critical_multiplier=critical_multiplier,
base_accuracy=accuracy,
attack_range=self.base_range,
)
events = []
if combat_result["hit"]:
damage = combat_result["damage"]
is_critical = combat_result["is_critical"]
target_died = combat_result["target_died"]
attack_event = Event(
event_type="attack_hit",
actor_id=actor.id,
target_id=target.id,
data={
"damage": damage,
"is_critical": is_critical,
"damage_type": self.damage_type,
"target_died": target_died,
**combat_result["damage_info"],
},
)
events.append(attack_event)
if target_died:
death_event = Event(
event_type="entity_death",
actor_id=target.id,
target_id=actor.id,
data={"killed_by": actor.id, "cause": "combat"},
)
events.append(death_event)
message = (
f"Killed {target.name} with {damage} {self.damage_type} damage"
)
if is_critical:
message += " (Critical Hit!)"
else:
message = f"Hit {target.name} for {damage} {self.damage_type} damage"
if is_critical:
message += " (Critical Hit!)"
return ActionResult.success(message, events)
else:
miss_event = Event(
event_type="attack_miss",
actor_id=actor.id,
target_id=target.id,
data={
"hit_chance": combat_result["hit_chance"],
"distance": combat_result["distance"],
},
)
events.append(miss_event)
return ActionResult.success(f"Missed attack on {target.name}", events)
def _get_weapon_stats(self, actor: Entity) -> tuple[int, float, float, float]:
weapon = actor.inventory.get_equipped_weapon()
if weapon:
damage = weapon.get_damage()
critical_chance = weapon.get_critical_chance()
critical_multiplier = weapon.get_critical_multiplier()
accuracy = 0.9
else:
damage = 5
critical_chance = 0.05
critical_multiplier = 2.0
accuracy = 0.8
modifiers = self.combat_resolver.get_combat_modifiers(actor)
damage += modifiers["attack_bonus"]
critical_chance += modifiers["critical_chance_bonus"]
damage = int(damage * modifiers["damage_multiplier"])
accuracy += modifiers["accuracy_bonus"]
return damage, critical_chance, critical_multiplier, accuracy
[docs]
def get_duration(self) -> int:
return 2
[docs]
def get_cost(self) -> ResourceCost:
return ResourceCost(stamina=3)
def __repr__(self) -> str:
return f"CombatAction(target={self.target_id}, type={self.damage_type})"
[docs]
class MeleeAttack(CombatAction):
def __init__(self, actor_id: int, target_id: int):
super().__init__(
actor_id=actor_id,
target_id=target_id,
damage_type=DamageType.PHYSICAL,
base_range=1.5, # Changed from 1.0 to allow diagonal attacks
)
[docs]
def can_execute(self, actor: Entity, world: World) -> bool:
weapon = actor.inventory.get_equipped_weapon()
if weapon and weapon.get_attack_type() not in ["melee", "weapon"]:
return False
return super().can_execute(actor, world)
[docs]
def get_cost(self) -> ResourceCost:
return ResourceCost(stamina=5)
[docs]
class RangedAttack(CombatAction):
def __init__(self, actor_id: int, target_id: int, weapon_range: float = 10.0):
super().__init__(
actor_id=actor_id,
target_id=target_id,
damage_type=DamageType.PHYSICAL,
base_range=weapon_range,
)
[docs]
def can_execute(self, actor: Entity, world: World) -> bool:
weapon = actor.inventory.get_equipped_weapon()
if not weapon or weapon.get_attack_type() != "ranged":
return False
self.base_range = weapon.get_range()
return super().can_execute(actor, world)
[docs]
def get_cost(self) -> ResourceCost:
return ResourceCost(stamina=4)
[docs]
class MagicAttack(CombatAction):
def __init__(
self,
actor_id: int,
target_id: int,
spell_type: str = DamageType.MAGICAL,
spell_range: float = 15.0,
):
super().__init__(
actor_id=actor_id,
target_id=target_id,
damage_type=spell_type,
base_range=spell_range,
)
[docs]
def can_execute(self, actor: Entity, world: World) -> bool:
weapon = actor.inventory.get_equipped_weapon()
if weapon:
if weapon.get_attack_type() == "magic":
self.base_range = weapon.get_range()
else:
self.base_range = 5.0
magic_cost = self._get_magic_cost(actor)
if actor.stats.magic < magic_cost:
return False
return super().can_execute(actor, world)
def _get_magic_cost(self, actor: Entity) -> int:
weapon = actor.inventory.get_equipped_weapon()
if weapon and weapon.get_attack_type() == "magic":
return weapon.get_magic_cost()
return 10
[docs]
def get_cost(self) -> ResourceCost:
magic_cost = 10
return ResourceCost(stamina=2, magic=magic_cost)
[docs]
class DefendAction(Action):
def __init__(self, actor_id: int, duration: int = 3):
super().__init__(actor_id)
self.defense_duration = duration
[docs]
def can_execute(self, actor: Entity, world: World) -> bool:
return not actor.has_status_effect("defending")
[docs]
def execute(self, actor: Entity, world: World) -> ActionResult:
if not self.can_execute(actor, world):
return ActionResult.failure("Already defending")
from ..entities.base import StatusEffect
defend_effect = StatusEffect(
name="defending",
duration=self.defense_duration,
effect_type="defense",
power=actor.stats.defense * 0.5,
)
actor.apply_status_effect(defend_effect)
event = Event(
event_type="defend",
actor_id=actor.id,
data={
"defense_bonus": defend_effect.power,
"duration": self.defense_duration,
},
)
return ActionResult.success("Entered defensive stance", [event])
[docs]
def get_duration(self) -> int:
return 1
[docs]
def get_cost(self) -> ResourceCost:
return ResourceCost(stamina=1)
[docs]
class FleeAction(Action):
def __init__(self, actor_id: int, flee_distance: int = 3):
super().__init__(actor_id)
self.flee_distance = flee_distance
[docs]
def can_execute(self, actor: Entity, world: World) -> bool:
return self.get_cost().can_afford(actor)
[docs]
def execute(self, actor: Entity, world: World) -> ActionResult:
if not self.can_execute(actor, world):
return ActionResult.failure("Cannot flee")
cost = self.get_cost()
if not cost.consume(actor):
return ActionResult.failure("Not enough stamina to flee")
current_x, current_y = actor.position
best_position = None
max_distance = 0
for dx in range(-self.flee_distance, self.flee_distance + 1):
for dy in range(-self.flee_distance, self.flee_distance + 1):
if dx == 0 and dy == 0:
continue
new_x, new_y = current_x + dx, current_y + dy
if not world.is_valid_position(new_x, new_y) or not world.is_passable(
new_x, new_y
):
continue
distance = math.sqrt(dx**2 + dy**2)
if distance > max_distance:
max_distance = distance
best_position = (new_x, new_y)
if best_position:
success = world.move_entity(actor, best_position[0], best_position[1])
if success:
event = Event(
event_type="flee",
actor_id=actor.id,
data={
"from": (current_x, current_y),
"to": best_position,
"distance": max_distance,
},
)
return ActionResult.success(f"Fled to {best_position}", [event])
return ActionResult.failure("Could not find safe position to flee to")
[docs]
def get_duration(self) -> int:
return 1
[docs]
def get_cost(self) -> ResourceCost:
return ResourceCost(stamina=10)