diff --git a/README.md b/README.md index 3ee48b2..af67e52 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Stepwise -## Concept +## Concepts -"Fun value", inspired by Undertale, activates different secrets in different runs. +* "Fun value", inspired by `Undertale`, activates different secrets in different runs. +* Color-based effects or biomes that only have an impact when in the FOV. +* gobbos, da red gobbo Stepwise needs a genre/setting. Some options are: diff --git a/source/actions.py b/source/actions.py index 9024e9f..cf52852 100644 --- a/source/actions.py +++ b/source/actions.py @@ -1,30 +1,58 @@ from engine import Engine - from entity import Entity class Action: - def apply(self, engine: Engine) -> None: + def apply(self, engine: Engine, entity: Entity) -> None: raise NotImplementedError() class QuitAction(Action): - def apply(self, engine: Engine) -> None: + def apply(self, engine: Engine, entity: Entity) -> None: raise SystemExit() -class MoveAction(Action): - def __init__(self, entity: Entity, xdir: int, ydir: int): + +class DirectionAction(Action): + def __init__(self, xdir: int, ydir: int): super().__init__() - self.entity = entity self.xdir = xdir self.ydir = ydir - def apply(self, engine: Engine) -> None: - x = self.entity.x + self.xdir - y = self.entity.y + self.ydir + def apply(self, engine: Engine, entity: Entity) -> None: + raise NotImplementedError() + + +class MovementAction(DirectionAction): + def apply(self, engine: Engine, entity: Entity) -> None: + dest_x = entity.x + self.xdir + dest_y = entity.y + self.ydir #bounds and collision checks - if not engine.floor_map.in_bounds(x, y): + if not engine.floor_map.in_bounds(dest_x, dest_y): return - if not engine.floor_map.tiles["walkable"][x, y]: + if not engine.floor_map.tiles["walkable"][dest_x, dest_y]: + return + if engine.floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True) is not None: return - self.entity.set_pos(x, y) + entity.set_pos(dest_x, dest_y) + +class MeleeAction(DirectionAction): + def apply(self, engine: Engine, entity: Entity) -> None: + dest_x = entity.x + self.xdir + dest_y = entity.y + self.ydir + + target = engine.floor_map.get_entity_at(dest_x, dest_y) + + if not target: + return + + print(f"You kicked the {target.name}, which did nothing") + +class MoveAction(DirectionAction): + def apply(self, engine: Engine, entity: Entity) -> None: + dest_x = entity.x + self.xdir + dest_y = entity.y + self.ydir + + if engine.floor_map.get_entity_at(dest_x, dest_y): + return MeleeAction(self.xdir, self.ydir).apply(engine, entity) + else: + return MovementAction(self.xdir, self.ydir).apply(engine, entity) diff --git a/source/engine.py b/source/engine.py index a53a067..b1aaab6 100644 --- a/source/engine.py +++ b/source/engine.py @@ -5,20 +5,18 @@ from tcod.console import Console from tcod.map import compute_fov from entity import Entity +import entity_types from floor_map import FloorMap #TODO: replace with "DungeonMap" class Engine: def __init__(self, floor_map: FloorMap): from event_handler import EventHandler #here to prevent circular imports self.event_handler = EventHandler(self) - - self.player = Entity(0, 0, "@", (255, 255, 255)) self.floor_map = floor_map - self.entities: Iterable[Entity] = [] - #spawn the player, add to render list - self.player.x, self.player.y = self.floor_map.spawn - self.entities.append(self.player) + #spawn the player object + self.player = entity_types.player.clone(self.floor_map.procgen_cache["spawn_x"], self.floor_map.procgen_cache["spawn_y"]) + self.floor_map.entities.add(self.player) #kick off the render self.update_fov() @@ -30,10 +28,15 @@ class Engine: if action is None: continue - action.apply(self) + action.apply(self, self.player) + self.handle_entities() self.update_fov() #update before the next action + def handle_entities(self) -> None: + for entity in self.floor_map.entities - {self.player}: + pass #run entity AI + def update_fov(self): self.floor_map.visible[:] = compute_fov( self.floor_map.tiles["transparent"], @@ -47,9 +50,7 @@ class Engine: def render(self, context: Context, console: Console) -> None: self.floor_map.render(console) - for entity in self.entities: - if self.floor_map.visible[entity.x, entity.y]: - console.print(entity.x, entity.y, entity.char, fg=entity.color) + console.print(self.player.x, self.player.y, self.player.char, fg=self.player.color) #send to the screen context.present(console) diff --git a/source/entity.py b/source/entity.py index 510b598..65b8ad8 100644 --- a/source/entity.py +++ b/source/entity.py @@ -1,11 +1,32 @@ -from typing import Tuple +from __future__ import annotations + +import copy +from typing import Tuple, TypeVar + +T = TypeVar("T", bound="Entity") class Entity: - def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]): + def __init__( + self, + x: int = 0, + y: int = 0, + char: str = "?", + color: Tuple[int, int, int] = (255, 255, 255), + name: str = "", + walkable: bool = True + ): self.x = x self.y = y self.char = char self.color = color + self.name = name + self.walkable = walkable + + def clone(self: T, x: int, y: int) -> T: + clone = copy.deepcopy(self) + clone.x = x + clone.y = y + return clone def set_pos(self, x: int, y: int) -> None: self.x = x diff --git a/source/entity_types.py b/source/entity_types.py new file mode 100644 index 0000000..ebaf557 --- /dev/null +++ b/source/entity_types.py @@ -0,0 +1,8 @@ +from entity import Entity + +player = Entity(char="@", color=(255, 255, 255), name="Player", walkable=False) + +#gobbos +gobbo = Entity(char="g", color=(30, 168, 41), name="Gobbo", walkable=False) +gobbo_red = Entity(char="g", color=(168, 41, 30), name="Red Gobbo", walkable=False) + diff --git a/source/event_handler.py b/source/event_handler.py index ca053c5..e4a2590 100644 --- a/source/event_handler.py +++ b/source/event_handler.py @@ -22,13 +22,13 @@ class EventHandler(tcod.event.EventDispatch[Action]): return QuitAction() case tcod.event.KeySym.UP: - return MoveAction(self.engine.player, xdir = 0, ydir = -1) + return MoveAction(xdir = 0, ydir = -1) case tcod.event.KeySym.DOWN: - return MoveAction(self.engine.player, xdir = 0, ydir = 1) + return MoveAction(xdir = 0, ydir = 1) case tcod.event.KeySym.LEFT: - return MoveAction(self.engine.player, xdir = -1, ydir = 0) + return MoveAction(xdir = -1, ydir = 0) case tcod.event.KeySym.RIGHT: - return MoveAction(self.engine.player, xdir = 1, ydir = 0) + return MoveAction(xdir = 1, ydir = 0) case _: return None \ No newline at end of file diff --git a/source/floor_map.py b/source/floor_map.py index 6686e12..cf4abec 100644 --- a/source/floor_map.py +++ b/source/floor_map.py @@ -1,28 +1,46 @@ -from typing import Tuple +from __future__ import annotations +from typing import Iterable, Optional import numpy as np - from tcod.console import Console import tile_types +from entity import Entity class FloorMap: - def __init__(self, width: int, height: int): - self.width = width - self.height = height + def __init__(self, width: int, height: int, entities: Iterable[Entity] = (), ): + #terrain stuff + self.width, self.height = width, height self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F") self.visible = np.full((width, height), fill_value=False, order="F") self.explored = np.full((width, height), fill_value=False, order="F") - self.spawn: Tuple[int, int] = 0, 0 + #contents stuff + self.entities = set(entities) + self.procgen_cache = {} #reserved for the procgen algorithm, otherwise ignored def in_bounds(self, x: int, y: int) -> bool: return 0 <= x < self.width and 0 <= y < self.height + def get_entity_at(self, x: int, y: int, unwalkable_only: bool = False) -> Optional[Entity]: + for entity in self.entities: + if entity.x == x and entity.y == y: + if unwalkable_only: + if not entity.walkable: + return entity + else: + return entity + + return None + def render(self, console: Console) -> None: console.rgb[0:self.width, 0:self.height] = np.select( condlist = [self.visible, self.explored], choicelist = [self.tiles["light"], self.tiles["dark"]], default = tile_types.SHROUD - ) \ No newline at end of file + ) + + for entity in self.entities: + if self.visible[entity.x, entity.y]: + console.print(entity.x, entity.y, entity.char, fg=entity.color) \ No newline at end of file diff --git a/source/procgen.py b/source/procgen.py index df09624..7a2136d 100644 --- a/source/procgen.py +++ b/source/procgen.py @@ -6,6 +6,7 @@ from typing import Iterator, List, Tuple import tcod import tile_types +import entity_types from floor_map import FloorMap #utils @@ -50,8 +51,39 @@ class RectangularRoom: self.y2 >= other.y1 ) +def spawn_monsters(room: RectangularRoom, floor_map: FloorMap, room_monsters_max: int) -> None: + monster_count = random.randint(0, room_monsters_max) + + #There can only be one + if "gobbo_red" not in floor_map.procgen_cache: + floor_map.procgen_cache["gobbo_red"] = False + + for i in range(monster_count): + #admittedly weird layout here, because player isn't in the entities list yet + x, y = floor_map.procgen_cache["spawn_x"], floor_map.procgen_cache["spawn_y"] + while x == floor_map.procgen_cache["spawn_x"] and y == floor_map.procgen_cache["spawn_y"]: + x = random.randint(room.x1 + 1, room.x2 - 1) + y = random.randint(room.y1 + 1, room.y2 - 1) + + #if there's no entity at that position + if not any(entity.x == x and entity.y == y for entity in floor_map.entities): + if not floor_map.procgen_cache["gobbo_red"] and random.random() < 0.2: + floor_map.procgen_cache["gobbo_red"] = True + floor_map.entities.add(entity_types.gobbo_red.clone(x, y)) + else: + floor_map.entities.add(entity_types.gobbo.clone(x, y)) + #generators -def generate_floor_map(map_width: int, map_height: int, room_width_max: int, room_height_max: int, room_width_min: int = 6, room_height_min: int = 6, room_count_max: int = 20) -> FloorMap: +def generate_floor_map( + map_width: int, + map_height: int, + room_width_max: int, + room_height_max: int, + room_width_min: int = 6, + room_height_min: int = 6, + room_count_max: int = 20, + room_monsters_max: int = 2 + ) -> FloorMap: #simplistic floor generator floor_map = FloorMap(map_width, map_height) @@ -72,11 +104,13 @@ def generate_floor_map(map_width: int, map_height: int, room_width_max: int, roo floor_map.tiles[new_room.inner] = tile_types.floor if len(rooms) == 0: - floor_map.spawn = new_room.center + floor_map.procgen_cache["spawn_x"], floor_map.procgen_cache["spawn_y"] = new_room.center else: for x, y in make_corridor(rooms[-1].center, new_room.center): floor_map.tiles[x, y] = tile_types.floor + spawn_monsters(new_room, floor_map, room_monsters_max) + rooms.append(new_room) return floor_map \ No newline at end of file