from __future__ import annotations
import logging
from typing import Dict, List, Optional, Tuple, Type, TYPE_CHECKING
import numpy as np
import snecs
from snecs.typedefs import EntityID
from scripts.engine.core import query, utility
from scripts.engine.core.component import FOV, Physicality, Position
from scripts.engine.internal.constant import (
Direction,
DirectionType,
Height,
TileTag,
TileTagType,
TravelMethod,
TravelMethodType,
)
from scripts.engine.world_objects.game_map import GameMap
from scripts.engine.world_objects.tile import Tile
if TYPE_CHECKING:
from typing import List, Optional, Tuple
from scripts.engine.internal.action import Skill
__all__ = ["get_game_map"]
########################### LOCAL DEFINITIONS ##########################
serialise = snecs.serialize_world
deserialise = snecs.deserialize_world
move_world = snecs.ecs.move_world
############################# GET - RETURN AN EXISTING SOMETHING ###########################
[docs]def get_game_map() -> GameMap:
"""
Get current game_map. Raises AttributeError if game_map doesnt exist.
"""
from scripts.engine.internal.data import store
if store.current_game_map:
game_map = store.current_game_map
else:
raise AttributeError("get_game_map: Tried to get the game_map but there isnt one.")
return game_map
[docs]def get_tile(tile_pos: Tuple[int, int]) -> Tile:
"""
Get the tile at the specified location. Raises exception if out of bounds or doesnt exist.
"""
from scripts.engine.internal.data import store
game_map = store.current_game_map # not using get_game_map for performance
assert isinstance(game_map, GameMap)
x, y = tile_pos
try:
tile = game_map.tile_map[x][y]
except IndexError:
raise IndexError(f"Tried to get tile({x},{y}), which doesnt exist.")
if _is_tile_in_bounds(tile, game_map):
return tile
else:
raise IndexError(f"Tried to get tile({x},{y}), which is out of bounds.")
[docs]def get_tiles(start_pos: Tuple[int, int], coords: List[Tuple[int, int]]) -> List[Tile]:
"""
Get multiple tiles based on starting position and coordinates given. Coords are relative to start
position given.
"""
start_x, start_y = start_pos
from scripts.engine.internal.data import store
game_map = store.current_game_map # not using get_game_map for performance
assert isinstance(game_map, GameMap)
tiles = []
for coord in coords:
x = coord[0] + start_x
y = coord[1] + start_y
# make sure it is in bounds
tile = get_tile((x, y))
if _is_tile_in_bounds(tile, game_map):
tiles.append(game_map.tile_map[x][y])
return tiles
[docs]def get_direction(start_pos: Tuple[int, int], target_pos: Tuple[int, int]) -> DirectionType:
"""
Get the direction between two locations.
"""
start_tile = get_tile(start_pos)
target_tile = get_tile(target_pos)
if start_tile and target_tile:
dir_x = target_tile.x - start_tile.x
dir_y = target_tile.y - start_tile.y
else:
# at least one of the tiles is out of bounds so return centre
return Direction.CENTRE
# handle any mistaken values coming in
dir_x = utility.clamp(dir_x, -1, 1)
dir_y = utility.clamp(dir_y, -1, 1)
return dir_x, dir_y # type: ignore
[docs]def get_entity_blocking_movement_map() -> np.ndarray:
"""
Return a Numpy array of bools, True for blocking and False for open
"""
game_map = get_game_map()
blocking_map = np.zeros((game_map.width, game_map.height), dtype=bool, order="F")
for entity, (pos, physicality) in query.position_and_physicality:
assert isinstance(physicality, Physicality)
assert isinstance(pos, Position)
if physicality.blocks_movement:
blocking_map[pos.x, pos.y] = True
return blocking_map
[docs]def get_a_star_path(start_pos: Tuple[int, int], target_pos: Tuple[int, int]) -> List[List[int]]:
"""
Get a list of coords that dictates the path between 2 entities.
"""
from scripts.engine.core.matter import create_pathfinder
pathfinder = create_pathfinder()
# add points
pathfinder.add_root(start_pos)
path = pathfinder.path_to(target_pos).tolist()
assert isinstance(path, list)
return path
[docs]def get_a_star_direction(start_pos: Tuple[int, int], target_pos: Tuple[int, int]) -> Optional[DirectionType]:
"""
Use a* pathfinding to get a direction from one entity to another. Does not allow diagonals.
"""
path = get_a_star_path(start_pos, target_pos)
# if there is a path then return direction
if path:
_start_pos = path[0]
next_pos = path[1]
move_dir = get_direction(_start_pos, next_pos) # type: ignore # list instead of tuple is fine
return move_dir
return None
[docs]def get_reflected_direction(
active_entity: EntityID, current_pos: Tuple[int, int], target_direction: Tuple[int, int]
) -> DirectionType:
"""
Use surrounding walls to understand how the object should be reflected.
"""
current_x, current_y = current_pos
dir_x, dir_y = target_direction
# work out position of adjacent walls
adj_tile = get_tile((current_x, current_y - dir_y))
if adj_tile:
collision_adj_y = tile_has_tag(active_entity, adj_tile, TileTag.BLOCKED_MOVEMENT)
else:
# found no tile
collision_adj_y = True
adj_tile = get_tile((current_x - dir_x, current_y))
if adj_tile:
collision_adj_x = tile_has_tag(active_entity, adj_tile, TileTag.BLOCKED_MOVEMENT)
else:
# found no tile
collision_adj_x = True
# where did we collide?
if collision_adj_x:
if collision_adj_y:
# hit a corner, bounce back towards entity
dir_x *= -1
dir_y *= -1
else:
# hit horizontal wall, reverse y direction
dir_y *= -1
else:
if collision_adj_y:
# hit a vertical wall, reverse x direction
dir_x *= -1
else: # not collision_adj_x and not collision_adj_y:
# hit a single piece, on the corner, bounce back towards entity
dir_x *= -1
dir_y *= -1
return dir_x, dir_y # type: ignore
[docs]def get_euclidean_distance(start_pos: Tuple[int, int], target_pos: Tuple[int, int]) -> float:
"""
Get distance from an xy position towards another location. Expected tuple in the form of (x, y).
This returns a float indicating the straight line distance between the two points.
"""
dx = target_pos[0] - start_pos[0]
dy = target_pos[1] - start_pos[1]
import math # only used in this method
return math.sqrt(dx ** 2 + dy ** 2)
[docs]def get_chebyshev_distance(start_pos: Tuple[int, int], target_pos: Tuple[int, int]):
"""
Get distance from an xy position towards another location. Expected tuple in the form of (x, y).
This returns an int indicating the number of tile moves between the two points.
"""
from scipy import spatial # only used in this method
distance = spatial.distance.chebyshev(start_pos, target_pos)
return distance
def _get_furthest_free_position(
active_entity: EntityID,
start_pos: Tuple[int, int],
target_direction: Tuple[int, int],
max_distance: int,
travel_type: TravelMethodType,
) -> Tuple[int, int]:
"""
Checks each position in a line and returns the last position that doesnt block movement. If no position in
range blocks movement then the last position checked is returned. If all positions in range block movement
then starting position is returned.
"""
start_x = start_pos[0]
start_y = start_pos[1]
dir_x = target_direction[0]
dir_y = target_direction[1]
current_x = start_x
current_y = start_y
free_x = start_x
free_y = start_y
check_for_target = False
# determine travel method
if travel_type == TravelMethod.STANDARD:
# standard can hit a target at any point during travel
check_for_target = True
elif travel_type == TravelMethod.ARC:
# throw can only hit target at end of travel
check_for_target = False
# determine impact location N.B. +1 to make inclusive as starting from 1
for distance in range(1, max_distance + 1):
# allow throw to hit target
if travel_type == TravelMethod.ARC and distance == max_distance + 1:
check_for_target = True
# get current position
current_x = start_x + (dir_x * distance)
current_y = start_y + (dir_y * distance)
tile = get_tile((current_x, current_y))
if tile:
# did we hit something causing standard to stop
if tile_has_tag(active_entity, tile, TileTag.BLOCKED_MOVEMENT):
# if we're ready to check for a target, do so
if check_for_target:
# we hit something, go back to last free tile
current_x = free_x
current_y = free_y
break
else:
free_x = current_x
free_y = current_y
return current_x, current_y
[docs]def get_cast_positions(
entity: EntityID, target_pos: Position, skills: List[Type[Skill]]
) -> Dict[Type[Skill], List[Tuple[int, int]]]:
"""
Check through list of skills to find unblocked cast positions to target
"""
skill_dict: Dict[Type[Skill], List[Tuple[int, int]]] = {}
# loop all skills and all directions
for skill in skills:
skill_dict[skill] = []
for direction in skill.target_directions:
# work out from target pos, reversing direction as we are working from target, not towards them
for _range in range(1, skill.range + 1): # +1 to be inclusive
x = target_pos.x + (-_range * direction[0])
y = target_pos.y + (-_range * direction[1])
# check tile is open and in vision
tile = get_tile((x, y))
has_tags = tile_has_tags(entity, tile, [TileTag.IS_VISIBLE, TileTag.OPEN_SPACE])
has_self = tile_has_tag(entity, tile, TileTag.SELF)
if has_tags or has_self:
skill_dict[skill].append((x, y))
else:
# space blocked so further out spaces will be no good either
break
return skill_dict
############################# QUERIES - CAN, IS, HAS - RETURN BOOL #############################
[docs]def tile_has_tag(active_entity: EntityID, tile: Tile, tag: TileTagType) -> bool:
"""
Check if a given tag applies to the tile. True if tag applies.
"""
from scripts.engine.internal.data import store
game_map = store.current_game_map # not using get_game_map for performance
assert isinstance(game_map, GameMap)
# before we even check tags, lets confirm it is in bounds
if not _is_tile_in_bounds(tile, game_map):
return False
if tag == TileTag.OPEN_SPACE:
# if nothing is blocking movement
if not tile.blocks_movement and not _tile_has_entity_blocking_movement(tile):
return True
else:
return False
elif tag == TileTag.BLOCKED_MOVEMENT:
# if anything is blocking
if tile.blocks_movement or _tile_has_entity_blocking_movement(tile):
return True
else:
return False
elif tag == TileTag.SELF:
# if entity on tile is same as active entity
if active_entity:
assert isinstance(active_entity, EntityID)
return _tile_has_specific_entity(tile, active_entity)
else:
logging.warning("tile_has_tag: Tried to get TileTag.SELF but gave no active_entity.")
return False
elif tag == TileTag.OTHER_ENTITY:
# if entity on tile is not active entity
if active_entity:
assert isinstance(active_entity, EntityID)
# check both possibilities. either the tile containing the active entity or not
return _tile_has_other_entities(tile, active_entity)
else:
logging.warning("tile_has_tag: Tried to get TileTag.OTHER_ENTITY but gave no active_entity.")
return False
elif tag == TileTag.NO_ENTITY:
# if the tile has no entity
return not _tile_has_any_entity(tile)
elif tag == TileTag.ANY:
# if the tile is anything at all
return True
elif tag == TileTag.IS_VISIBLE:
# if player can see the tile
return _is_tile_visible_to_entity(tile, active_entity, game_map)
elif tag == TileTag.NO_BLOCKING_TILE:
# if tile isnt blocking movement
if _is_tile_in_bounds(tile, game_map):
return not tile.blocks_movement
else:
return False
elif tag == TileTag.ACTOR:
# if the tile contains an actor
for query_result in query.actors:
# this is a very dirty way to access the position of a query result
# I don't want to scan for the position component since it would severely bog down performance if many
# entities exist and this function is used frequently
# structuring the results as a dict may be beneficial
position = query_result[1][0]
assert isinstance(position, Position)
if (position.x, position.y) == (tile.x, tile.y):
return True
return False
# If we've hit here it must be false!
logging.warning(f"tile_has_tag: TileTag {tag} does not exist.")
return False
def _is_tile_blocking_sight(tile: Tile) -> bool:
"""
Check if a tile is blocking sight
"""
# assumes tile are min or max height only.
if tile.height == Height.MAX:
return True
return False
def _is_tile_visible_to_entity(tile: Tile, entity: EntityID, game_map: GameMap) -> bool:
"""
Check if the specified tile is visible to the entity
"""
from scripts.engine.core.matter import get_entitys_component
fov_map = get_entitys_component(entity, FOV).map
light_map = game_map.light_map
# combine maps
visible_map = fov_map & light_map # type: ignore
return bool(visible_map[tile.x, tile.y])
def _is_tile_in_bounds(tile: Tile, game_map: GameMap) -> bool:
"""
Check if specified tile is in the map.
"""
return (0 <= tile.x < game_map.width) and (0 <= tile.y < game_map.height)
def _tile_has_any_entity(tile: Tile) -> bool:
"""
Check if the specified tile has an entity on it
"""
from scripts.engine.core.matter import get_entities_on_tile
return len(get_entities_on_tile(tile)) > 0
def _tile_has_other_entities(tile: Tile, active_entity: EntityID) -> bool:
"""
Check if the specified tile has other entities apart from the provided active entity
"""
from scripts.engine.core.matter import get_entities_on_tile
entities_on_tile = get_entities_on_tile(tile)
active_entity_is_on_tile = active_entity in entities_on_tile
return (len(entities_on_tile) > 0 and not active_entity_is_on_tile) or (
len(entities_on_tile) > 1 and active_entity_is_on_tile
)
def _tile_has_specific_entity(tile: Tile, active_entity: EntityID) -> bool:
"""
Check if the specified tile has the specified entity on it
"""
from scripts.engine.core.matter import get_entities_on_tile
return active_entity in get_entities_on_tile(tile)
def _tile_has_entity_blocking_movement(tile: Tile) -> bool:
x = tile.x
y = tile.y
# Any entities that block movement?
from scripts.engine.core.matter import get_components
for entity, (position, physicality) in get_components([Position, Physicality]):
assert isinstance(position, Position)
assert isinstance(physicality, Physicality)
if (x, y) in position and physicality.blocks_movement:
return True
return False
def _tile_has_entity_blocking_sight(tile: Tile, active_entity: EntityID) -> bool:
x = tile.x
y = tile.y
from scripts.engine.core.matter import entity_has_component
if entity_has_component(active_entity, Physicality):
from scripts.engine.core.matter import get_entitys_component
viewer_height = get_entitys_component(active_entity, Physicality).height
else:
# viewer has no height, assume everything blocks
return True
# Any entities that block sight?
from scripts.engine.core.matter import get_components
for entity, (position, physicality) in get_components([Position, Physicality]):
assert isinstance(position, Position)
assert isinstance(physicality, Physicality)
if (x, y) in position and physicality.height > viewer_height:
return True
return False
[docs]def is_direction_blocked(entity: EntityID, dir_x: int, dir_y: int) -> bool:
"""
Checks if the entity will collide with something when trying to move in the provided direction. Returns True
if blocked.
"""
collides = False
direction_name = utility.value_to_member((dir_x, dir_y), Direction)
from scripts.engine.core.matter import get_entitys_component
position = get_entitys_component(entity, Position)
for coordinate in position.coordinates:
target_x = coordinate[0] + dir_x
target_y = coordinate[1] + dir_y
target_tile = get_tile((target_x, target_y))
# check a tile was returned
is_tile_blocking_movement = False
if target_tile:
is_tile_blocking_movement = tile_has_tag(entity, target_tile, TileTag.BLOCKED_MOVEMENT)
# check if tile is blocked
if is_tile_blocking_movement:
# find out who is blocking
for other_entity, (pos, physicality) in query.position_and_physicality:
assert isinstance(pos, Position)
assert isinstance(physicality, Physicality)
if other_entity != entity and physicality.blocks_movement and (target_x, target_y) in pos.coordinates:
# blocked by entity
from scripts.engine.core.matter import get_name
blockers_name = get_name(other_entity)
name = get_name(entity)
logging.debug(
f"'{name}' tried to move {direction_name} to ({target_x},{target_y}) but was blocked"
f" by '{blockers_name}'. "
)
break
collides = True
return collides