51 lines
2.2 KiB
Python
51 lines
2.2 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Callable
|
|
|
|
from engine.character import Character
|
|
from engine.defs import DefRegistry
|
|
from engine.world import World
|
|
from resources.logic.combat import DiceRoller
|
|
from resources.logic.turrets import TURRET_BEHAVIOR_ID, turret_ai_behavior
|
|
|
|
# Seconds between wander steps, randomized so a room full of NPCs doesn't lock-step.
|
|
WANDER_INTERVAL = (2.0, 5.0)
|
|
_WANDER_DIRECTIONS: list[tuple[int, int]] = [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
|
|
|
|
|
def _wander(character: Character, world: World, registry: DefRegistry, now: float, rng: DiceRoller) -> None:
|
|
"""The simplest useful example behavior: every so often, take one random step in a
|
|
cardinal direction (a no-op if that step is blocked - see World.try_move). This exists to
|
|
prove the ai-slot framework actually reaches into the world, not as a real patrol/pathing
|
|
AI - most NPCs will want a purpose-built behavior of their own registered alongside it.
|
|
"""
|
|
assert character.ai is not None
|
|
if now < character.ai.state.get("next_move_at", 0.0):
|
|
return
|
|
dx, dy = rng.choice(_WANDER_DIRECTIONS)
|
|
world.try_move(character, dx, dy, 0)
|
|
character.ai.state["next_move_at"] = now + rng.uniform(*WANDER_INTERVAL)
|
|
|
|
|
|
# behavior_id (see EntityDef.ai_behavior_id / AIController.behavior_id) -> tick function. Add a
|
|
# new NPC behavior by writing its function and registering it here - run_ai_tick needs no
|
|
# changes to pick it up.
|
|
AI_BEHAVIORS: dict[str, Callable[[Character, World, DefRegistry, float, DiceRoller], None]] = {
|
|
"wander": _wander,
|
|
TURRET_BEHAVIOR_ID: turret_ai_behavior,
|
|
}
|
|
|
|
|
|
def run_ai_tick(character: Character, world: World, registry: DefRegistry, now: float, rng: DiceRoller) -> None:
|
|
"""Ticks whichever behavior `character.ai.behavior_id` names, if any. A no-op for a
|
|
player-controlled or otherwise inert Character (`ai is None`) and for an unrecognized
|
|
behavior_id, so a bad/typo'd def can never crash the tick loop. Meant to be called once per
|
|
Character per elapsed game tick (see main.py's PLAYING per-tick block).
|
|
"""
|
|
if character.ai is None:
|
|
return
|
|
behavior = AI_BEHAVIORS.get(character.ai.behavior_id)
|
|
if behavior is None:
|
|
return
|
|
behavior(character, world, registry, now, rng)
|