| from __future__ import annotations |
|
|
| import math |
|
|
| from world_simulator.domain import Npc, Vec3 |
|
|
| GRID_BLOCKS = 20 |
| BLOCK_SIZE = 4.0 |
|
|
| |
| 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) |
|
|