Source code for simulation_framework.src.entities.npc

from __future__ import annotations

import random
from typing import TYPE_CHECKING, List, Optional, Tuple

from ..items.loot_table import LootTable
from .base import Entity
from .stats import Stats

if TYPE_CHECKING:
    from ..core.world import World


[docs] class NPC(Entity): def __init__( self, position: Tuple[int, int], name: str = "NPC", npc_type: str = "neutral", stats: Optional[Stats] = None, loot_table: Optional[LootTable] = None, spawn_point: Optional[Tuple[int, int]] = None, tether_radius: int = 10, ): super().__init__(position, name, stats or Stats()) self.npc_type = npc_type self.loot_table = loot_table or LootTable() self.spawn_point = spawn_point or position self.tether_radius = tether_radius self.aggro_range = 5 self.target_id: Optional[int] = None self.last_seen_target_pos: Optional[Tuple[int, int]] = None self.patrol_points: List[Tuple[int, int]] = [] self.current_patrol_index = 0 self.idle_timer = 0 self.aggro_cooldown = 0 self.current_action = None # Track current action like agents do
[docs] def update(self, world: World) -> None: self.update_status_effects() self._update_aggro_cooldown() if not self.stats.is_alive(): return if self._is_too_far_from_spawn(): self._return_to_spawn(world) return if self.target_id and self._should_continue_combat(world): self._combat_behavior(world) elif self.npc_type == "aggressive": self._scan_for_targets(world) if not self.target_id: self._idle_behavior(world)
def _update_aggro_cooldown(self) -> None: if self.aggro_cooldown > 0: self.aggro_cooldown -= 1 def _is_too_far_from_spawn(self) -> bool: spawn_x, spawn_y = self.spawn_point current_x, current_y = self.position distance = ((current_x - spawn_x) ** 2 + (current_y - spawn_y) ** 2) ** 0.5 return distance > self.tether_radius def _return_to_spawn(self, world: World) -> None: self.target_id = None self.last_seen_target_pos = None def _should_continue_combat(self, world: World) -> bool: if not self.target_id: return False target = world.entities.get(self.target_id) if not target or not target.stats.is_alive(): self.target_id = None return False distance = self.distance_to(target) if distance > self.aggro_range * 2: self.target_id = None return False return True def _combat_behavior(self, world: World) -> None: target = world.entities.get(self.target_id) if not target: self.target_id = None return self.last_seen_target_pos = target.position distance = self.distance_to(target) # Only set new action if we don't have one already if not self.current_action or not self.current_action.is_active: # Initiate combat if in range if distance <= 1.5: from ..actions.combat import MeleeAttack attack = MeleeAttack(self.id, self.target_id) if attack.can_execute(self, world): self.current_action = attack self.current_action.start(world.current_tick) # Start the action else: # Move closer to target from ..actions.movement import PathfindAction pathfind = PathfindAction(self.id, target.position) if pathfind.can_execute(self, world): self.current_action = pathfind self.current_action.start(world.current_tick) # Start the action def _scan_for_targets(self, world: World) -> None: if self.aggro_cooldown > 0: return current_x, current_y = self.position for entity in world.entities.values(): if entity.id == self.id: continue if not entity.stats.is_alive(): continue distance = self.distance_to(entity) if distance <= self.aggro_range: if hasattr(entity, "inventory"): self.target_id = entity.id self.aggro_cooldown = 5 break def _idle_behavior(self, world: World) -> None: self.idle_timer += 1 if self.idle_timer >= random.randint(10, 30): self.idle_timer = 0 self._perform_idle_action(world) def _perform_idle_action(self, world: World) -> None: if self.patrol_points: self._patrol(world) else: self._wander(world) def _patrol(self, world: World) -> None: if not self.patrol_points: return target_point = self.patrol_points[self.current_patrol_index] if self.distance_to_position(*target_point) < 1.5: self.current_patrol_index = (self.current_patrol_index + 1) % len( self.patrol_points ) else: from ..actions.movement import PathfindAction pathfind = PathfindAction(self.id, target_point) if pathfind.can_execute(self, world): pathfind.execute(self, world) def _wander(self, world: World) -> None: # Only set new action if we don't have one already if not self.current_action or not self.current_action.is_active: from ..actions.movement import WanderAction wander = WanderAction(self.id, self.spawn_point, max_distance=3) if wander.can_execute(self, world): self.current_action = wander self.current_action.start(world.current_tick) # Start the action
[docs] def set_patrol_route(self, points: List[Tuple[int, int]]) -> None: self.patrol_points = points self.current_patrol_index = 0
[docs] def add_patrol_point(self, point: Tuple[int, int]) -> None: self.patrol_points.append(point)
[docs] def set_aggressive(self, aggressive: bool = True) -> None: self.npc_type = "aggressive" if aggressive else "neutral"
[docs] def set_target(self, target: Entity) -> None: """Set a target for combat - called by simulation when agent comes within aggro range""" if target and target.stats.is_alive(): self.target_id = target.id self.last_seen_target_pos = target.position self.aggro_cooldown = 5
[docs] def on_death(self, killer: Optional[Entity] = None) -> None: if killer and self.loot_table: self._drop_loot(killer) self._register_for_respawn()
def _drop_loot(self, killer: Entity) -> None: luck_modifier = getattr(killer, "luck", 0) * 0.01 loot_items = self.loot_table.generate_loot(luck_modifier) for item, quantity in loot_items: remaining = killer.inventory.add_item(item, quantity) if remaining > 0: pass def _register_for_respawn(self) -> None: pass
[docs] def get_threat_level(self) -> str: total_stats = ( self.stats.max_health + self.stats.attack_power + self.stats.defense ) if total_stats < 50: return "weak" elif total_stats < 100: return "normal" elif total_stats < 200: return "strong" else: return "elite"
[docs] def is_hostile_to(self, entity: Entity) -> bool: if self.npc_type == "aggressive": return hasattr(entity, "inventory") return False
def __repr__(self) -> str: status = "alive" if self.stats.is_alive() else "dead" target_info = f", target={self.target_id}" if self.target_id else "" return f"NPC(id={self.id}, name='{self.name}', type={self.npc_type}, {status}{target_info})"
[docs] def create_basic_goblin(position: Tuple[int, int]) -> NPC: stats = Stats( max_health=30, health=30, max_stamina=40, stamina=40, attack_power=8, defense=2, speed=6, ) loot_table = LootTable.create_basic_monster_loot() return NPC( position=position, name="Goblin", npc_type="aggressive", stats=stats, loot_table=loot_table, )
[docs] def create_forest_wolf(position: Tuple[int, int]) -> NPC: stats = Stats( max_health=45, health=45, max_stamina=60, stamina=60, attack_power=12, defense=3, speed=8, ) loot_table = LootTable() from ..items.consumable import Consumable from ..items.item import Item meat = Item( id=200, name="Wolf Meat", item_type="material", properties={"resource_type": "meat"}, value=8, description="Fresh wolf meat", max_stack_size=20, ) loot_table.add_entry(meat, 0.8, 1, 3) loot_table.add_entry(Consumable.create_food("Wolf Meat"), 0.4, 1, 2) wolf = NPC( position=position, name="Forest Wolf", npc_type="aggressive", stats=stats, loot_table=loot_table, ) wolf.aggro_range = 7 return wolf
[docs] def create_peaceful_villager(position: Tuple[int, int]) -> NPC: stats = Stats( max_health=25, health=25, max_stamina=30, stamina=30, attack_power=3, defense=1, speed=4, ) return NPC(position=position, name="Villager", npc_type="neutral", stats=stats)