1
0
Fork 0

Add movement AI to npcs

This commit is contained in:
Timothy Warren 2022-01-10 13:21:17 -05:00
parent 50ddf8cc01
commit be433a9449
6 changed files with 110 additions and 13 deletions

View File

@ -34,6 +34,11 @@ class EscapeAction(Action):
raise SystemExit()
class WaitAction(Action):
def perform(self) -> None:
pass
class ActionWithDirection(Action):
def __init__(self, entity, dx: int, dy: int):
super().__init__(entity)

View File

@ -1,15 +1,20 @@
from __future__ import annotations
from typing import List, Tuple
from typing import List, Tuple, TYPE_CHECKING
import numpy as np # type: ignore
import tcod
from actions import Action
from actions import Action, MeleeAction, MovementAction, WaitAction
from components.base_component import BaseComponent
if TYPE_CHECKING:
from entity import Actor
class BaseAI(Action, BaseComponent):
entity: Actor
def get_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
"""Compute and return a path to the target position.
@ -38,3 +43,33 @@ class BaseAI(Action, BaseComponent):
# Convert from List[List[int]] to List[Tuple[int, int]].
return [(index[0], index[1]) for index in path]
class HostileEnemy(BaseAI):
def __init__(self, entity: Actor):
super().__init__(entity)
self.path: List[Tuple[int, int]] = []
def perform(self) -> None:
target = self.engine.player
dx = target.x - self.entity.x
dy = target.y - self.entity.y
# Chebyshev distance
distance = max(abs(dx), abs(dy))
if self.engine.game_map.visible[self.entity.x, self.entity.y]:
if distance <= 1:
return MeleeAction(self.entity, dx, dy).perform()
self.path = self.get_path_to(target.x, target.y)
if self.path:
dest_x, dest_y = self.path.pop(0)
return MovementAction(
self.entity,
dest_x - self.entity.x,
dest_y - self.entity.y
).perform()
return WaitAction(self.entity).perform()

View File

@ -24,8 +24,9 @@ class Engine:
self.player = player
def handle_enemy_turns(self) -> None:
for entity in self.game_map.entities - {self.player}:
print(f'The {entity.name} wonders when it will get to take a real turn.')
for entity in set(self.game_map.actors) - {self.player}:
if entity.ai:
entity.ai.perform()
def update_fov(self) -> None:
"""Recompute the visible area based on the player's point of view."""

View File

@ -1,9 +1,11 @@
from __future__ import annotations
import copy
from typing import Optional, Tuple, TypeVar, TYPE_CHECKING
from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
if TYPE_CHECKING:
from components.ai import BaseAI
from components.fighter import Fighter
from game_map import GameMap
T = TypeVar("T", bound="Entity")
@ -61,3 +63,35 @@ class Entity:
# Move the entity by a given amount
self.x += dx
self.y += dy
class Actor(Entity):
def __init__(
self,
*,
x: int = 0,
y: int = 0,
char: str = "?",
color: Tuple[int, int, int] = (255, 255, 255),
name: str = "<Unamed>",
ai_cls: Type[BaseAI],
fighter: Fighter
):
super().__init__(
x=x,
y=y,
char=char,
color=color,
name=name,
blocks_movement=True
)
self.ai = Optional[BaseAI] = ai_cls(self)
self.fighter = fighter
self.fighter.entity = self
@property
def is_alive(self) -> bool:
"""Returns True as long as this actor can perform actions."""
return bool(self.ai)

View File

@ -1,21 +1,26 @@
from entity import Entity
from components.ai import HostileEnemy
from components.fighter import Fighter
from entity import Actor
player = Entity(
player = Actor(
char="@",
color=(255, 255, 255),
name="PLayer",
blocks_movement=True
ai_cls=HostileEnemy,
fighter=Fighter(hp=30, defense=2, power=5),
)
orc = Entity(
orc = Actor(
char="o",
color=(63, 127, 63),
name="Orc",
blocks_movement=True
ai_cls=HostileEnemy,
fighter=Fighter(hp=10, defense=0, power=3),
)
troll = Entity(
troll = Actor(
char="T",
color=(0, 127, 0),
name="Troll",
blocks_movement=True
ai_cls=HostileEnemy,
fighter=Fighter(hp=16, defense=1, power=4)
)

View File

@ -1,10 +1,11 @@
from __future__ import annotations
from typing import Iterable, Optional, TYPE_CHECKING
from typing import Iterable, Iterator, Optional, TYPE_CHECKING
import numpy as np # type: ignore
from tcod.console import Console
from entity import Actor
import tile_types
if TYPE_CHECKING:
@ -36,6 +37,15 @@ class GameMap:
order="F"
) # Tiles the player has seen before
@property
def actors(self) -> Iterator[Actor]:
"""Iterate over this map's living actors."""
yield from (
entity
for entity in self.entities
if isinstance(entity, Actor) and entity.is_alive
)
def get_blocking_entity_at_location(
self,
location_x: int,
@ -51,6 +61,13 @@ class GameMap:
return None
def get_actor_at_location(self, x: int, y: int) -> Optional[Actor]:
for actor in self.actors:
if actor.x == x and actor.y == y:
return actor
return None
def in_bounds(self, x: int, y: int) -> bool:
"""Return True if x and y are inside the bounds of the map."""
return 0 <= x < self.width and 0 <= y < self.height