Inventory is visible, and dropping items is enabled

Menu windows can be nested inside each other.
This commit is contained in:
Kayne Ruse 2025-03-30 21:37:48 +11:00
parent 13ac477bad
commit 5e52e166b1
7 changed files with 225 additions and 68 deletions

View File

@ -16,7 +16,7 @@ Here's a few potential options, which will absolutely change over time:
* Fairy Tales and Fables (Brothers Grimm)
* Secret Agent Espionage
* Mythic Odyssey
* Dreamlands/Cthulhu Horror
* Dreamlands/Cthulhu Horror/Gothic Horror (accessible only during the full/new moon)
* Stargates/Sliders
* Isekai Protag Syndrome
* Gunslingers (Wild West)

View File

@ -5,6 +5,10 @@
A banana taped to a wall?
A note taped to a wall that says "I. O. U. 1 Banana"
Could also have an art gallery room
"Dead Souls" - Each time you die, your past souls are collected, and you might be able to spend them to unlock something new.
A lock in one dimension needs a key (password) in another?
It could be interesting, not necssarily a good or fun idea, but if you had another "point" or currency. Where if you spend the first one you get some kind of "cursed knowledge" point and that affects the run somehow? So the more you use your forbidden knowledge the more weird things get?
If you use IRL time and date as a mechanic, go big or go home. Maybe the horror dimension is only accessible during full/new moons?
## Healing

View File

@ -1,8 +1,9 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from typing import List, Optional, TYPE_CHECKING
import colors
from floor_map import FloorMap
from inventory import Inventory
if TYPE_CHECKING:
from engine import Engine
@ -123,6 +124,7 @@ class PickupAction(BaseAction):
y = self.entity.y
floor_map: FloorMap = self.entity.floor_map
engine: Engine = floor_map.engine
item_stack: List[Entity] = floor_map.get_all_entities_at(x, y, items_only=True)
@ -131,22 +133,55 @@ class PickupAction(BaseAction):
elif len(item_stack) == 1:
floor_map.entities.remove(item_stack[0])
self.entity.inventory.insert(item_stack[0])
floor_map.engine.message_log.add_message(f"you picked up a(n) {item_stack[0].name}", color=colors.terminal_light)
engine.message_log.add_message(f"you picked up a(n) {item_stack[0].name}", 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_stack:#not pythonic, IDC
for item in item_stack:
options.append(item.name)
from event_handlers import OptionSelector #circular imports are a pain
floor_map.engine.event_handler = OptionSelector(
floor_map.engine,
floor_map.engine.event_handler,
options,
lambda x: (
floor_map.entities.remove(item_stack[x]),
self.entity.inventory.insert(item_stack[x]),
floor_map.engine.message_log.add_message(f"you picked up a(n) {item_stack[x].name}", color=colors.terminal_light)
),
engine.event_handler = OptionSelector(
engine=floor_map.engine,
parent_handler=engine.event_handler,
title="Pick Up Item",
options=options,
callback=lambda x: self.pickup_callback(engine, floor_map, self.entity, item_stack[x])
)
return True
#utils
def pickup_callback(self, engine: Engine, floor_map: FloorMap, entity: Entity, item: Entity) -> None:
floor_map.entities.remove(item)
entity.inventory.insert(item)
engine.message_log.add_message(f"you picked up a(n) {item.name}", color=colors.terminal_light)
class DropAction(BaseAction):
"""Drop an item from an entity's inventory at the entity's location"""
index: int
def __init__(self, entity: Entity, index: int):
"""override the base __init__"""
super().__init__(entity)
self.index = index
def perform(self) -> bool:
x = self.entity.x
y = self.entity.y
inventory: Inventory = self.entity.inventory
floor_map: FloorMap = self.entity.floor_map
engine: Engine = floor_map.engine
item: Entity = inventory.withdraw(self.index)
item.x = x
item.y = y
floor_map.entities.add(item)
engine.message_log.add_message(f"you dropped a(n) {item.name}", color=colors.terminal_light)
return True

View File

@ -20,7 +20,7 @@ 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)
self.event_handler = GameplayHandler(self, None)
self.mouse_position = (0, 0)
#map

View File

@ -29,5 +29,7 @@ gobbo_red = Entity(
name = "Red Gobbo",
walkable = False,
ai_class = AggressiveWhenSeen,
stats = Stats(hp = 5, attack = 2, defense = 5),
stats = Stats(hp = 5, attack = 1, defense = 0), #this guy can't catch a break
)
#TODO: healing potion, spawned in the map

View File

@ -10,10 +10,12 @@ from actions import (
BumpAction,
WaitAction,
PickupAction,
DropAction,
)
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
#input options
MOVE_KEYS = {
@ -76,16 +78,19 @@ CURSOR_CONFIRM_KEYS = {
tcod.event.KeySym.SPACE,
}
#the event handlers are one part of the engine
#the event handlers are a big part of the engine
class EventHandler(tcod.event.EventDispatch[BaseAction]):
engine: Engine
parent_handler: EventHandler
def __init__(self, engine: Engine):
def __init__(self,engine: Engine, parent_handler: EventHandler):
super().__init__()
self.engine = engine
self.parent_handler = parent_handler
def render(self, console: tcod.console.Console) -> None:
pass #no-op
if self.parent_handler:
self.parent_handler.render(console)
#callbacks
def ev_quit(self, event: tcod.event.Quit) -> Optional[BaseAction]:
@ -113,10 +118,11 @@ class GameplayHandler(EventHandler):
player = self.engine.player
#player input
#special keys
if key == tcod.event.KeySym.ESCAPE:
return QuitAction()
#gameplay keys
if key in MOVE_KEYS:
xdir, ydir = MOVE_KEYS[key]
return BumpAction(player, xdir = xdir, ydir = ydir)
@ -127,14 +133,16 @@ class GameplayHandler(EventHandler):
if key in PICKUP_KEYS:
return PickupAction(player)
if key == tcod.event.KeySym.o: #TODO: temove this
self.engine.event_handler = OptionSelector(self.engine, self, ["zero", "one", "two", "three"], lambda x: print("You chose", x)) #TODO: remove this
#menu keys
if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde
self.engine.event_handler = LogHistoryViewer(self.engine, self)
# if key == tcod.event.KeySym.TAB:
# self.engine.event_handler = InventoryViewer(self.engine, self) #TODO: deal with this
if key == tcod.event.KeySym.TAB:
self.engine.event_handler = InventoryViewer(self.engine, self, player)
#debugging
if key == tcod.event.KeySym.o: #TODO: remove this
self.engine.event_handler = OptionSelector(self.engine, self, options=["zero", "one", "two", "three"], callback=lambda x: print("You chose", x))
def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
if self.engine.floor_map.in_bounds(event.tile.x, event.tile.y):
@ -157,20 +165,17 @@ class GameOverHandler(EventHandler):
class LogHistoryViewer(EventHandler):
baseEventHandler: EventHandler
def __init__(self, engine: Engine, baseEventHandler: EventHandler):
super().__init__(engine)
self.baseEventHandler = baseEventHandler
self.log_length = len(engine.message_log.messages)
self.cursor = self.log_length - 1
def __init__(self, engine: Engine, parent_handler: EventHandler):
super().__init__(engine, parent_handler)
self.length = len(engine.message_log.messages)
self.cursor = self.length - 1 #start at the bottom
def render(self, console: tcod.console.Console) -> None:
super().render(console)
log_console = tcod.console.Console(console.width - 6, console.height - 6)
#rendering a nice log window
#rendering a nice window
log_console.draw_frame(
0,0, log_console.width, log_console.height,
# "╔═╗║ ║╚═╝"
@ -189,44 +194,150 @@ class LogHistoryViewer(EventHandler):
log_console.width - 4, log_console.height - 4,
self.engine.message_log.messages[:self.cursor + 1]
)
log_console.blit(console, 3, 3) #into the middle
log_console.blit(console, 3, 3)
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
if event.sym in CURSOR_SCROLL_KEYS:
adjust = CURSOR_SCROLL_KEYS[event.sym]
if adjust < 0 and self.cursor == 0:
pass #do nothing
elif adjust > 0 and self.cursor == self.log_length - 1:
elif adjust > 0 and self.cursor == self.length - 1:
pass #do nothing
else:
self.cursor = max(0, min(self.log_length - 1, self.cursor + adjust))
self.cursor = max(0, min(self.length - 1, self.cursor + adjust))
elif event.sym == tcod.event.KeySym.HOME:
self.cursor = 0
elif event.sym == tcod.event.KeySym.END:
self.cursor = self.log_length - 1
self.cursor = self.length - 1
else:
#return to the game
self.engine.event_handler = self.baseEventHandler
#return to the game - where's the any key?
self.engine.event_handler = self.parent_handler
#generic tool
class OptionSelector(EventHandler):
baseEventHandler: EventHandler
def __init__(self, engine: Engine, baseEventHandler: EventHandler, options: List[str], callback: function):
super().__init__(engine)
self.baseEventHandler = baseEventHandler
self.options = options
self.callback = callback
self.length = len(options)
class InventoryViewer(EventHandler):
def __init__(self, engine: Engine, parent_handler: EventHandler, entity: Entity): #this entity's inventory
super().__init__(engine, parent_handler)
self.entity = entity
self.length = len(self.entity.inventory.contents)
self.cursor = 0
def render(self, console: tcod.console.Console) -> None:
super().render(console)
select_console = tcod.console.Console(console.width - 20, console.height - 16)
inner_console = tcod.console.Console(console.width - 6, console.height - 6)
#rendering a nice list window
#rendering a nice window
inner_console.draw_frame(
0,0, inner_console.width, inner_console.height,
# "╔═╗║ ║╚═╝"
decoration="\\x/x x/x\\",
fg=colors.terminal_dark, bg=colors.black
)
inner_console.print_box(
0, 0, inner_console.width, inner_console.height,
string = "Inventory",
alignment=tcod.constants.CENTER,
fg=colors.terminal_light, bg=colors.black
)
#render the cursor & inventory contents
offset: int = 0
for item in self.entity.inventory.contents:
inner_console.print(
4, 2 + offset,
string = item.name,
fg=colors.terminal_light, bg=colors.black,
)
offset += 1
if self.length > 0:
inner_console.print(2, 2 + self.cursor, string = ">", fg=colors.terminal_light, bg=colors.black)
else:
#if inventory is empty, show a default message
inner_console.print_box(
0,inner_console.height // 2, inner_console.width, inner_console.height,
string = "EMPTY",
fg=colors.terminal_dark, bg=colors.black,
alignment=tcod.constants.CENTER
)
inner_console.blit(console, 3, 3)
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
if event.sym in CURSOR_SCROLL_KEYS:
adjust = CURSOR_SCROLL_KEYS[event.sym]
if adjust < 0 and self.cursor == 0:
pass #do nothing
elif adjust > 0 and self.cursor == self.length - 1:
pass #do nothing
else:
self.cursor = max(0, min(self.length - 1, self.cursor + adjust))
#extra size check, so empty inventories still work
elif self.length > 0 and event.sym in CURSOR_CONFIRM_KEYS:
#drop an item form this entity's inventory
item: Entity = self.entity.inventory.access(self.cursor)
self.engine.event_handler = OptionSelector(self.engine, self,
title=f"Drop The {item.name}?",
options=["Yes", "No"],
callback=lambda x: self.drop_callback(x)
)
elif event.sym == tcod.event.KeySym.HOME:
self.cursor = 0
elif event.sym == tcod.event.KeySym.END:
self.cursor = self.length - 1
else:
#return to the game - where's the any key?
self.engine.event_handler = self.parent_handler
#utils
def drop_callback(self, answer: int) -> Optional[BaseAction]:
#process the answer, giving the signal of what to do
if answer == 0:
c = self.cursor
#bounds
self.length -= 1
if self.cursor >= self.length:
self.cursor = self.length - 1
return DropAction(self.entity, c)
#generic tools
class OptionSelector(EventHandler):
def __init__(
self,
engine: Engine,
parent_handler: EventHandler,
*,
options: List[str],
callback: function,
title: str = "Select Option",
margin_x: int = 10,
margin_y: int = 8
):
super().__init__(engine, parent_handler)
self.options = options
self.callback = callback
self.length = len(options)
self.cursor = 0
#graphical prettiness
self.title = title
self.margin_x = margin_x
self.margin_y = margin_y
def render(self, console: tcod.console.Console) -> None:
super().render(console)
select_console = tcod.console.Console(console.width - self.margin_x*2, console.height - self.margin_y*2)
#rendering a nice window
select_console.draw_frame(
0,0, select_console.width, select_console.height,
# "╔═╗║ ║╚═╝"
@ -235,13 +346,13 @@ class OptionSelector(EventHandler):
)
select_console.print_box(
0, 0, select_console.width, select_console.height,
string = "Select One",
string = self.title,
alignment=tcod.constants.CENTER,
fg=colors.terminal_light, bg=colors.black
)
#render the cursor & options
offset = 0
offset: int = 0
for option in self.options:
select_console.print(
4, 2 + offset,
@ -252,7 +363,7 @@ class OptionSelector(EventHandler):
select_console.print(2, 2 + self.cursor, string = ">", fg=colors.terminal_light, bg=colors.black)
select_console.blit(console, 10, 8)
select_console.blit(console, self.margin_x, self.margin_y)
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
if event.sym in CURSOR_SCROLL_KEYS:
@ -265,9 +376,8 @@ class OptionSelector(EventHandler):
self.cursor = max(0, min(self.length - 1, self.cursor + adjust))
elif event.sym in CURSOR_CONFIRM_KEYS:
#got the answer
self.callback(self.cursor)
self.engine.event_handler = self.baseEventHandler
self.engine.event_handler = self.parent_handler
return self.callback(self.cursor) #confirm this selection, and exit
elif event.sym == tcod.event.KeySym.HOME:
self.cursor = 0
@ -275,4 +385,4 @@ class OptionSelector(EventHandler):
self.cursor = self.length - 1
else:
#return to the game
self.engine.event_handler = self.baseEventHandler
self.engine.event_handler = self.parent_handler

View File

@ -1,31 +1,37 @@
from __future__ import annotations
from typing import Optional, Set, TYPE_CHECKING
from typing import List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from entity import Entity
class Inventory:
"""Handles inventory for an Entity"""
_contents: Set[Entity]
_contents: List[Entity]
def __init__(self, contents: Set[Entity] = set()):
def __init__(self, contents: List[Entity] = []):
self._contents = contents
def insert(self, entity: Entity) -> bool:
if entity in self._contents:
return False
self._contents.add(entity)
self._contents.append(entity)
return True
def access(self, key: str) -> Optional[Entity]:
return self._contents[key]
def access(self, index: int) -> Optional[Entity]:
if index < 0 or index >= len(self._contents):
return None
else:
return self._contents[index]
def remove(self, key: str) -> Optional[Entity]:
item = self._contents[key]
self._contents.remove(key)
return item
def withdraw(self, index: int) -> Optional[Entity]:
if index < 0 or index >= len(self._contents):
return None
else:
return self._contents.pop(index)
@property
def contents(self) -> Set[Entity]:
def contents(self) -> List[Entity]:
return self._contents
#TODO: items need a weight, inventory needs a max capacity