from __future__ import annotations import math from world_simulator.domain import Npc, Vec3 GRID_BLOCKS = 20 BLOCK_SIZE = 4.0 # Common melee reach for every attacker: NPC vs NPC, NPC vs beast, beast vs NPC. ATTACK_RADIUS = 0.5 * BLOCK_SIZE TALK_RADIUS = 3.0 * BLOCK_SIZE VISIBLE_RADIUS = 5.0 * BLOCK_SIZE MAX_WALK_DISTANCE = 1.0 * BLOCK_SIZE def is_alive(npc: Npc) -> bool: return npc.health > 0 def distance_between(first: Npc, second: Npc) -> float: return math.hypot(first.position.x - second.position.x, first.position.z - second.position.z) def vec_distance(first: Vec3, second: Vec3) -> float: """Planar distance between two world positions (x/z plane).""" return math.hypot(first.x - second.x, first.z - second.z) def move_toward( origin: Vec3, destination: Vec3, step: float, *, half_width: float | None = None, half_depth: float | None = None, ) -> Vec3: """Move ``origin`` up to ``step`` world units toward ``destination``.""" delta_x = destination.x - origin.x delta_z = destination.z - origin.z distance = math.hypot(delta_x, delta_z) if distance == 0 or distance <= step: target_x, target_z = destination.x, destination.z else: scale = step / distance target_x = origin.x + (delta_x * scale) target_z = origin.z + (delta_z * scale) return _bounded_vec(target_x, target_z, half_width=half_width, half_depth=half_depth) def move_away( origin: Vec3, threat: Vec3, step: float, *, half_width: float | None = None, half_depth: float | None = None, ) -> Vec3: """Move ``origin`` up to ``step`` world units directly away from ``threat``.""" delta_x = origin.x - threat.x delta_z = origin.z - threat.z distance = math.hypot(delta_x, delta_z) if distance == 0: delta_x = -1.0 if origin.x > 0 else 1.0 delta_z = 0.0 distance = 1.0 scale = step / distance return _bounded_vec( origin.x + (delta_x * scale), origin.z + (delta_z * scale), half_width=half_width, half_depth=half_depth, ) def _bounded_vec( x: float, z: float, *, half_width: float | None, half_depth: float | None, ) -> Vec3: if half_width is not None: x = _clamp(x, -half_width, half_width) if half_depth is not None: z = _clamp(z, -half_depth, half_depth) return Vec3(x=round(x, 3), y=0.0, z=round(z, 3)) _HOSTILE_KEYWORDS = frozenset( { "attack", "destroy", "eliminate", "fight", "harm", "hostile", "hunt", "hurt", "kill", "murder", "slay", "violent", "violence", } ) def has_hostile_intent(npc: Npc) -> bool: if not is_alive(npc): return False return _contains_any(npc.god_directive, _HOSTILE_KEYWORDS) or _contains_any( npc.intention, _HOSTILE_KEYWORDS, ) def walk_away_target( npc: Npc, threat: Npc, *, half_width: float, half_depth: float, ) -> Vec3: delta_x = npc.position.x - threat.position.x delta_z = npc.position.z - threat.position.z distance = math.hypot(delta_x, delta_z) if distance == 0: delta_x = -1.0 if npc.position.x > 0 else 1.0 delta_z = 0.0 distance = 1.0 scale = MAX_WALK_DISTANCE / distance return Vec3( x=round(_clamp(npc.position.x + (delta_x * scale), -half_width, half_width), 3), y=0.0, z=round(_clamp(npc.position.z + (delta_z * scale), -half_depth, half_depth), 3), ) def _contains_any(text: str | None, keywords: frozenset[str]) -> bool: if not text: return False lowered = text.lower() return any(keyword in lowered for keyword in keywords) def _clamp(value: float, minimum: float, maximum: float) -> float: return min(max(value, minimum), maximum)