From a7064e3db05fc2be2745e4c65a16f7af665399df Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Sat, 5 Apr 2025 18:59:22 +1100 Subject: [PATCH] Added ranged Useables, TileSelector Added ScrollOfLightning as an example. Not finished yet, confusion & fireball are still in part 9. --- dev-dimensions.md | 2 +- dev-notes.md | 1 + promo/screenshot_05.png | Bin 0 -> 14688 bytes promo/screenshot_06.png | Bin 0 -> 14713 bytes promo/screenshot_07.png | Bin 0 -> 14947 bytes source/actions.py | 33 ++++++--- source/colors.py | 38 ++++++---- source/engine.py | 6 +- source/entity.py | 4 + source/entity_types.py | 23 ++++-- source/event_handlers.py | 156 +++++++++++++++++++++++++++++++-------- source/main.py | 2 +- source/procgen.py | 5 +- source/stats.py | 6 +- source/useable.py | 45 +++++++++-- 15 files changed, 248 insertions(+), 73 deletions(-) create mode 100644 promo/screenshot_05.png create mode 100644 promo/screenshot_06.png create mode 100644 promo/screenshot_07.png diff --git a/dev-dimensions.md b/dev-dimensions.md index 1cec006..ff3590f 100644 --- a/dev-dimensions.md +++ b/dev-dimensions.md @@ -33,7 +33,7 @@ Gobbo Swarm Merchant Ship * A group of merchants capable of travelling between dimensions, will sell items from other worlds, including unique items not available elsewhere. -* For each run (perhaps, with a minimum level needed, so it can't be cheesed), a counter ticks down until zero, at which point the merchant ship is guaranteed to generate. The counter only resets when the player finds it. In essence, this guarantees the ship will be encountered on a semi-regular basis. +* For each run (perhaps, with a minimum level needed, so it can't be cheesed), a counter ticks down until zero, at which point the merchant ship is guaranteed to generate. The counter only resets when the player finds it. In essence, this guarantees the ship will be encountered on a semiregular basis. Contagion * A strange affliction, that turns people into zombies. Could also prevent the spawning of normally present monsters, to invoke a 28-days-later vibe. diff --git a/dev-notes.md b/dev-notes.md index b084b10..3b6a3d5 100644 --- a/dev-notes.md +++ b/dev-notes.md @@ -6,6 +6,7 @@ This file is a kind of scratch-pad for a multitude of ideas, that I can't implem https://rogueliketutorials.com/ https://python-tcod.readthedocs.io/en/latest/ +https://pyinstaller.org/en/stable/ ```bash #make the virtual environment diff --git a/promo/screenshot_05.png b/promo/screenshot_05.png new file mode 100644 index 0000000000000000000000000000000000000000..fac462a2babfed3c288eace5118ee4bf91357bcc GIT binary patch literal 14688 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 AOaK4? literal 0 HcmV?d00001 diff --git a/promo/screenshot_06.png b/promo/screenshot_06.png new file mode 100644 index 0000000000000000000000000000000000000000..b1fa9d3e34d46eb8022be94564a1346bd9696cfa GIT binary patch literal 14713 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 literal 0 HcmV?d00001 diff --git a/promo/screenshot_07.png b/promo/screenshot_07.png new file mode 100644 index 0000000000000000000000000000000000000000..c19127d7ae1e2df1bf5f91402bfe05eb1f20e6f7 GIT binary patch literal 14947 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 AQ2+n{ literal 0 HcmV?d00001 diff --git a/source/actions.py b/source/actions.py index 888ac32..9a051a4 100644 --- a/source/actions.py +++ b/source/actions.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import List, Optional, TYPE_CHECKING +from typing import List, TYPE_CHECKING import colors from floor_map import FloorMap @@ -8,6 +8,7 @@ from useable import BaseUseable if TYPE_CHECKING: from engine import Engine + from event_handlers import OptionSelector, TileSelector from entity import Entity class BaseAction: @@ -143,8 +144,6 @@ class PickupAction(BaseAction): self.entity.inventory.insert(item) engine.message_log.add_message(msg, color=colors.terminal_light) else: - from event_handlers import OptionSelector #circular imports are a pain - #build an options list options: List[str] = [] for item in item_pile: @@ -268,17 +267,29 @@ class UsageAction(BaseAction): item: Entity = inventory.access(self.index) usable: BaseUseable = item.useable - if usable.apply(self.target.stats) and usable.is_stack_empty(): - #remove the item from the inventory - inventory.discard(self.index) + #TODO: also check visibility? - if self.display_callback: - self.display_callback(-1) + #check range + distance = self.entity.get_distance_to(self.target.x, self.target.y) + + if usable.minimum_range > distance: + engine.message_log.add_message("You're too close to use this!", color=colors.terminal_light) + return False + + elif usable.maximum_range < distance: + engine.message_log.add_message("You're too far to use this!", color=colors.terminal_light) + return False + + elif usable.apply(self.target.stats) and usable.is_stack_empty(): + #remove the item from the inventory + inventory.discard(self.index) + + if self.display_callback: + self.display_callback(-1) msg: str = usable.get_used_msg(item.name) if not msg: - msg = f"you used a(n) {item.name}" + msg = f"What is a(n) {item.name}?" engine.message_log.add_message(msg, color=colors.terminal_light) - - return True \ No newline at end of file + return True diff --git a/source/colors.py b/source/colors.py index cea4140..43bff93 100644 --- a/source/colors.py +++ b/source/colors.py @@ -1,16 +1,31 @@ -#Standard colors -white = (0xFF, 0xFF, 0xFF) -black = (0x0, 0x0, 0x0) +#url: https://en.wikipedia.org/wiki/Web_colors -red = (0xFF, 0, 0) -green = (0, 0xFF, 0) -blue = (0, 0, 0xFF) +#CSS standard colors +white = (0xFF, 0xFF, 0xFF) +silver = (0xC0, 0xC0, 0xC0) +gray = (0x80, 0x80, 0x80) +black = (0x00, 0x00, 0x00) +red = (0xFF, 0x00, 0x00) +maroon = (0x80, 0x00, 0x00) +yellow = (0xFF, 0xFF, 0x00) +olive = (0x80, 0x80, 0x00) +lime = (0x00, 0xFF, 0x00) +green = (0x00, 0x80, 0x00) +aqua = (0x00, 0xFF, 0xFF) #identical to cyan +teal = (0x00, 0x80, 0x80) +blue = (0x00, 0x00, 0xFF) +navy = (0x00, 0x00, 0x80) +fuchsia = (0xFF, 0x00, 0xFF) +purple = (0x80, 0x00, 0x80) #identical to magenta -yellow = (0xFF, 0xFF, 0) -magenta = (0xFF, 0, 0xFF) -cyan = (0, 0xFF, 0xFF) +#CSS extended colors (incomplete selection, may be expanded later) +pink = (0xFF, 0xC0, 0xCB) +orange = (0xFF, 0xA5, 0x00) +deep_sky_blue = (0x00, 0xBF, 0xFF) +navajo_white = (0xFF, 0xDE, 0xAD) #misnomer +goldenrod = (0xDA, 0xA5, 0x20) -#gameboy DMG-01, according to Wikipedia's CSS +#gameboy DMG-01 gameboy_00 = (0x29, 0x41, 0x39) gameboy_01 = (0x39, 0x59, 0x4a) gameboy_02 = (0x5a, 0x79, 0x42) @@ -19,6 +34,3 @@ gameboy_03 = (0x7b, 0x82, 0x10) #terminal-like terminal_light = (200, 200, 200) terminal_dark = (100, 100, 100) - -#extended colors -orange = (0xFF, 0xA5, 0x00) \ No newline at end of file diff --git a/source/engine.py b/source/engine.py index 151cc61..ff6050b 100644 --- a/source/engine.py +++ b/source/engine.py @@ -19,8 +19,8 @@ class Engine: def __init__(self, *, floor_map: FloorMap, initial_log: List[Message] = None, ui_width: int = None, ui_height: int = None): #events - from event_handlers import GameplayHandler - self.event_handler = GameplayHandler(self, None) + from event_handlers import GameplayViewer + self.event_handler = GameplayViewer(self, None) self.mouse_position = (0, 0) #map @@ -47,7 +47,7 @@ class Engine: self.update_fov() if self.event_handler.handle_events(context): - self.handle_entities() #TODO: what 'game state'? + self.handle_entities() self.handle_rendering(context, console) diff --git a/source/entity.py b/source/entity.py index 040f251..7d3c6c5 100644 --- a/source/entity.py +++ b/source/entity.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import math from typing import Optional, Tuple, Type, TYPE_CHECKING from ai import BaseAI @@ -72,6 +73,9 @@ class Entity: self.x = x self.y = y + def get_distance_to(self, x: int, y: int) -> float: + return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2) + #monster-specific stuff def is_alive(self) -> bool: return bool(self.ai) diff --git a/source/entity_types.py b/source/entity_types.py index a218699..47e66b4 100644 --- a/source/entity_types.py +++ b/source/entity_types.py @@ -2,12 +2,13 @@ from entity import Entity from ai import BaseAI, AggressiveWhenSeen from stats import Stats from inventory import Inventory -from useable import PotionOfHealing +import useable +import colors #player and utils player = Entity( char = "@", - color = (255, 255, 255), + color = colors.white, name = "Player", walkable = False, ai_class = BaseAI, #TODO: remove this or dummy it out @@ -18,7 +19,7 @@ player = Entity( #monsters - gobbos gobbo = Entity( char = "g", - color = (30, 168, 41), + color = colors.lime, name = "Gobbo", walkable = False, ai_class = AggressiveWhenSeen, @@ -27,7 +28,7 @@ gobbo = Entity( gobbo_red = Entity( char = "g", - color = (168, 41, 30), + color = colors.red, name = "Red Gobbo", walkable = False, ai_class = AggressiveWhenSeen, @@ -37,9 +38,19 @@ gobbo_red = Entity( #items - conumables potion_of_healing = Entity( char = "!", - color = (0, 0, 255), + color = colors.deep_sky_blue, name = "Potion of Healing", walkable = True, - useable=PotionOfHealing(current_stack=1, maximum_stack=255, consumable=True), + useable=useable.PotionOfHealing(consumable=True, current_stack=1, maximum_stack=255), ) +scroll_of_lightning = Entity( + char = "!", + color = colors.goldenrod, + name = "Scroll of Lightning", + walkable = True, + useable=useable.ScrollOfLightning(consumable=True, current_stack=1, maximum_stack=255, minimum_range=0, maximum_range=6), +) + +#TODO: scroll of confusion, using "confused AI" +#TODO: scroll of fireball, dealing AOE damage \ No newline at end of file diff --git a/source/event_handlers.py b/source/event_handlers.py index 081e2a3..fdab855 100644 --- a/source/event_handlers.py +++ b/source/event_handlers.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import List, Optional, TYPE_CHECKING +from typing import List, Optional, Tuple, TYPE_CHECKING import tcod @@ -15,9 +15,14 @@ from actions import ( UsageAction, ) +from useable import ( + BaseUseable, +) + if TYPE_CHECKING: from engine import Engine from entity import Entity + from floor_map import FloorMap #input options MOVE_KEYS = { @@ -63,6 +68,7 @@ WAIT_KEYS = { PICKUP_KEYS = { tcod.event.KeySym.COMMA, + tcod.event.KeySym.SPACE, } CURSOR_SCROLL_KEYS = { @@ -80,6 +86,12 @@ CURSOR_CONFIRM_KEYS = { tcod.event.KeySym.SPACE, } +TILE_SCROLL_KEYS = MOVE_KEYS #copied + +# TILE_SELECTOR_KEYS + +TILE_CONFIRM_KEYS = CURSOR_CONFIRM_KEYS #copied + #the event handlers are a big part of the engine class EventHandler(tcod.event.EventDispatch[BaseAction]): engine: Engine @@ -114,7 +126,22 @@ class EventHandler(tcod.event.EventDispatch[BaseAction]): return result -class GameplayHandler(EventHandler): +class GameoverViewer(EventHandler): + """Game over, man, GAME OVER!""" + def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]: + key = event.sym #SDL stuff, neat. + + #player input + if key == tcod.event.KeySym.ESCAPE: + return QuitAction() + + if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde + self.engine.event_handler = LogHistoryViewer(self.engine, self) + + return None + + +class GameplayViewer(EventHandler): def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]: key = event.sym #SDL stuff, neat. @@ -142,34 +169,33 @@ class GameplayHandler(EventHandler): if key == tcod.event.KeySym.TAB: self.engine.event_handler = InventoryViewer(self.engine, self, player) - #debugging - can hook this up to more later + #debugging - for various controls and testing needs if (event.mod & tcod.event.Modifier.CTRL) and key == tcod.event.KeySym.d: self.engine.event_handler = OptionSelector( self.engine, self, - title="Debug Selector", - options=["Zero", "One", "Two", "Three"], - callback=lambda x: self.engine.message_log.add_message(f"DBG: You selected {x}", colors.orange), - margin_x=20, - margin_y=12, + title = "Debug Selector", + options = ["Tile Selector"], + callback = lambda x: self.dbg_callback(x), + margin_x = 20, + margin_y = 12, ) def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None: if self.engine.floor_map.in_bounds(event.tile.x, event.tile.y): - self.engine.mouse_location = event.tile.x, event.tile.y + self.engine.mouse_location = (event.tile.x, event.tile.y) + def dbg_callback(self, selected: int) -> Optional[BaseAction]: + if selected == 0: + player: Entity = self.engine.player -class GameOverHandler(EventHandler): - """Game over, man, GAME OVER!""" - def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]: - key = event.sym #SDL stuff, neat. - - #player input - if key == tcod.event.KeySym.ESCAPE: - return QuitAction() - - if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde - self.engine.event_handler = LogHistoryViewer(self.engine, self) + self.engine.event_handler = TileSelector( + self.engine, + self, + floor_map = self.engine.floor_map, + initial_pointer = (player.x, player.y), + callback=lambda: None + ) return None @@ -342,25 +368,44 @@ class InventoryViewer(EventHandler): return None #the selector does the change def use(self) -> Optional[BaseAction]: - """Drop the item at the cursor's position, and adjust the cursor if needed.""" + """Use the item at the cursor's position.""" if self.length > 0: - index = self.cursor + item: Entity = self.entity.inventory.access(self.cursor) + usable: BaseUseable = item.useable - return UsageAction(self.entity, index, self.entity, lambda x: self.adjust_length(x)) + #for ranged items, delegate to the tile selector + if usable.maximum_range > 0: + self.engine.event_handler = TileSelector( + self.engine, + parent_handler=self.engine.event_handler, + floor_map = self.engine.floor_map, + initial_pointer = (self.entity.x, self.entity.y), + callback=lambda x, y: self.use_at_range(x, y) + ) + else: + #non-ranged items target the entity + return UsageAction(self.entity, self.cursor, self.entity, lambda x: self.adjust_length(x)) + + def use_at_range(self, target_x: int, target_y: int) -> Optional[BaseAction]: + #TODO: For now, just target living entities + targets: List[Entity] = list(filter(lambda entity: entity.is_alive() and entity.x == target_x and entity.y == target_y, self.engine.floor_map.entities)) + + #TODO: skip targeting for AOE + #TODO: close the inventory if you've used a consumable? + if len(targets) > 0: + return UsageAction(self.entity, self.cursor, targets.pop(), lambda x: self.adjust_length(x)) + else: + self.engine.message_log.add_message("No target found.", colors.yellow) def drop_partial_stack(self, amount: int) -> Optional[BaseAction]: """Drop part of an item stack at the cursor's position, and adjust the cursor if needed.""" if self.length > 0: - index = self.cursor - - return DropPartialStackAction(self.entity, index, amount, lambda x: self.adjust_length(x)) + return DropPartialStackAction(self.entity, self.cursor, amount, lambda x: self.adjust_length(x)) def drop(self) -> Optional[BaseAction]: """Drop the item at the cursor's position, and adjust the cursor if needed.""" if self.length > 0: - index = self.cursor - - return DropAction(self.entity, index, lambda x: self.adjust_length(x)) + return DropAction(self.entity, self.cursor, lambda x: self.adjust_length(x)) def adjust_length(self, amount: int): self.length += amount @@ -446,3 +491,56 @@ class OptionSelector(EventHandler): else: #return to the game self.engine.event_handler = self.parent_handler + + +class TileSelector(EventHandler): + floor_map: FloorMap + cursor_x: int + cursor_y: int + callback: function + + def __init__( + self, + engine, + parent_handler, + *, + floor_map: FloorMap, + initial_pointer: Tuple[int, int], + callback: function, + ): + super().__init__(engine, parent_handler) + self.floor_map = floor_map + self.cursor_x, self.cursor_y = initial_pointer + self.callback = callback + + def render(self, console: tcod.console.Console) -> None: + #DON'T render the parent, instead, find and render the gameplay handler + parent: EventHandler = self.parent_handler + while parent and parent is not GameplayViewer: + parent = parent.parent_handler + if parent: + parent.render(console) + + #highlight via inverting colors + console.rgb["fg"][self.cursor_x, self.cursor_y] ^= 0xff + console.rgb["bg"][self.cursor_x, self.cursor_y] ^= 0xff + + def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]: + key = event.sym #SDL stuff, neat. + + #special keys + if key == tcod.event.KeySym.ESCAPE: + #return to the game + self.engine.event_handler = self.parent_handler + + #selection keys + elif key in TILE_SCROLL_KEYS: + xdir, ydir = TILE_SCROLL_KEYS[key] + + #because actions "only" change the game state, rather than selecting something, just move the cursor from here + self.cursor_x += xdir + self.cursor_y += ydir + + elif key in TILE_CONFIRM_KEYS: + self.engine.event_handler = self.parent_handler + return self.callback(self.cursor_x, self.cursor_y) diff --git a/source/main.py b/source/main.py index 2f30fe4..2840229 100755 --- a/source/main.py +++ b/source/main.py @@ -38,7 +38,7 @@ def main() -> None: Message(" Text Log: Backtick Items: Tab", colors.terminal_dark), Message(" Quit: Esc", colors.terminal_dark), Message(" ~ ~ ~", colors.terminal_dark), - Message("Welcome to the Cave of Gobbos!", colors.cyan), + Message("Welcome to the Cave of Gobbos!", colors.teal), ] ) diff --git a/source/procgen.py b/source/procgen.py index 4ce7052..a0eb4a4 100644 --- a/source/procgen.py +++ b/source/procgen.py @@ -95,7 +95,10 @@ def spawn_items(floor_map: FloorMap, room: RectangularRoom, room_items_max: int, #if there's no entity at that position (not really needed for walkable entities) if not any(entity.x == x and entity.y == y for entity in floor_map.entities): - entity_types.potion_of_healing.spawn(x, y, floor_map) + if random.random() < 0.8: + entity_types.scroll_of_lightning.spawn(x, y, floor_map) + else: + entity_types.potion_of_healing.spawn(x, y, floor_map) floor_map.procgen_cache["item_count"] += 1 #generators diff --git a/source/stats.py b/source/stats.py index 36b9f08..8b4307d 100644 --- a/source/stats.py +++ b/source/stats.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING import colors -from event_handlers import GameOverHandler +from event_handlers import GameoverViewer from useable import Unuseable if TYPE_CHECKING: @@ -40,11 +40,11 @@ class Stats: engine: Engine = self.entity.floor_map.engine if self.entity is engine.player and self.entity.ai: #handle game-over states - engine.event_handler = GameOverHandler(engine) + engine.event_handler = GameoverViewer(engine, engine.event_handler) #TODO: more elegant way to handle GameOver than effectively freezing engine.message_log.add_message("You died.", colors.red) else: - engine.message_log.add_message(f"The {self.entity.name} died", colors.yellow) + engine.message_log.add_message(f"The {self.entity.name} died", colors.green) #transform into a dead body #TODO: dummied in a "usable" to let dead objects be treated like items diff --git a/source/useable.py b/source/useable.py index 7c7e66f..c8d689b 100644 --- a/source/useable.py +++ b/source/useable.py @@ -7,15 +7,29 @@ if TYPE_CHECKING: from stats import Stats class BaseUseable: - """Base type for useable items, with various utilities""" + """ + Base type for useable items, with various utilities. + + Please note that distances are calculated with the Pythagorean theorem, so a maximum range of `1` won't work in diagonally adjacent tiles. + """ + consumable: bool current_stack: int maximum_stack: int - consumable: bool + minimum_range: int + maximum_range: int - def __init__(self, *, current_stack: int = 1, maximum_stack: int = -1, consumable: bool = False): + def __init__(self, *, + consumable: bool = False, + current_stack: int = 1, + maximum_stack: int = -1, + minimum_range: int = 0, + maximum_range: int = -1, + ): + self.consumable = consumable self.current_stack = current_stack self.maximum_stack = maximum_stack - self.consumable = consumable + self.minimum_range = minimum_range + self.maximum_range = maximum_range def apply(self, stats: Stats) -> bool: """ @@ -87,4 +101,25 @@ class PotionOfHealing(BaseUseable): return self.reduce_stack() def get_used_msg(self, appearance: str) -> Optional[str]: - return f"You restored {self.__last_roll} health." + if self.__last_roll >= 0: + return f"The {appearance} restored {self.__last_roll} health." + else: + return None + + +class ScrollOfLightning(BaseUseable): + """Deals 2d4 damage when applied.""" + __last_roll: int = -1 + + def apply(self, stats: Stats) -> bool: + self.__last_roll = roll_dice(2, 4) + stats.current_hp -= self.__last_roll + return self.reduce_stack() + + def get_used_msg(self, appearance: str) -> Optional[str]: + if self.__last_roll >= 0: + return f"The {appearance} dealt {self.__last_roll} damage." + else: + return None + +#TODO: "The gobbo died" and "You dealt X damage" are in the wrong order in the log \ No newline at end of file