Implemented usable potions, stackable items
This commit is contained in:
parent
e4a99900b5
commit
a4c112ce7b
@ -4,6 +4,7 @@ from typing import List, Optional, TYPE_CHECKING
|
||||
import colors
|
||||
from floor_map import FloorMap
|
||||
from inventory import Inventory
|
||||
from useable import BaseUseable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from engine import Engine
|
||||
@ -132,9 +133,13 @@ class PickupAction(BaseAction):
|
||||
if len(item_stack) == 0:
|
||||
return False
|
||||
elif len(item_stack) == 1:
|
||||
msg = "you picked up a(n) {item_stack[0].name}"
|
||||
if item_stack[0].useable.current_stack > 1:
|
||||
msg = msg = f"you picked up a stack of {item_stack[0].useable.current_stack} {item_stack[0].name}"
|
||||
|
||||
floor_map.entities.remove(item_stack[0])
|
||||
self.entity.inventory.insert(item_stack[0])
|
||||
engine.message_log.add_message(f"you picked up a(n) {item_stack[0].name}", color=colors.terminal_light)
|
||||
engine.message_log.add_message(msg, color=colors.terminal_light)
|
||||
else:
|
||||
from event_handlers import OptionSelector #circular imports are a pain
|
||||
|
||||
@ -155,18 +160,24 @@ class PickupAction(BaseAction):
|
||||
|
||||
#utils
|
||||
def pickup_callback(self, engine: Engine, floor_map: FloorMap, entity: Entity, item: Entity) -> None:
|
||||
msg = "you picked up a(n) {item.name}"
|
||||
if item.useable.current_stack > 1:
|
||||
msg = msg = f"you picked up a stack of {item.useable.current_stack} {item.name}"
|
||||
|
||||
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)
|
||||
engine.message_log.add_message(msg, color=colors.terminal_light)
|
||||
|
||||
class DropAction(BaseAction):
|
||||
"""Drop an item from an entity's inventory at the entity's location"""
|
||||
index: int
|
||||
display_callback: function
|
||||
|
||||
def __init__(self, entity: Entity, index: int):
|
||||
def __init__(self, entity: Entity, index: int, display_callback: function):
|
||||
"""override the base __init__"""
|
||||
super().__init__(entity)
|
||||
self.index = index
|
||||
self.display_callback = display_callback
|
||||
|
||||
def perform(self) -> bool:
|
||||
x = self.entity.x
|
||||
@ -183,6 +194,81 @@ class DropAction(BaseAction):
|
||||
|
||||
floor_map.entities.add(item)
|
||||
|
||||
if self.display_callback: #adjust the cursor
|
||||
self.display_callback(-1)
|
||||
|
||||
engine.message_log.add_message(f"you dropped a(n) {item.name}", color=colors.terminal_light)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DropPartialStackAction(BaseAction):
|
||||
"""Drop part of a stack from an entity's inventory at the entity's location"""
|
||||
index: int
|
||||
amount: int
|
||||
display_callback: function
|
||||
|
||||
def __init__(self, entity: Entity, index: int, amount: int, display_callback: function):
|
||||
"""override the base __init__"""
|
||||
super().__init__(entity)
|
||||
self.index = index
|
||||
self.amount = amount
|
||||
self.display_callback = display_callback
|
||||
|
||||
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.access(self.index)
|
||||
|
||||
new_item: Entity = item.spawn(x, y, floor_map)
|
||||
|
||||
item.useable.current_stack -= self.amount
|
||||
new_item.useable.current_stack = self.amount
|
||||
|
||||
if self.display_callback: #adjust the cursor
|
||||
self.display_callback(-1)
|
||||
|
||||
engine.message_log.add_message(f"you dropped a stack of {new_item.useable.current_stack} {new_item.name}", color=colors.terminal_light)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class UsageAction(BaseAction):
|
||||
"""Use an item from an entity's inventory, removing it if needed"""
|
||||
index: int
|
||||
target: Entity
|
||||
display_callback: function
|
||||
|
||||
def __init__(self, entity: Entity, index: int, target: Entity, display_callback: function):
|
||||
"""override the base __init__"""
|
||||
super().__init__(entity)
|
||||
self.index = index
|
||||
self.target = target
|
||||
self.display_callback = display_callback
|
||||
|
||||
def perform(self) -> bool:
|
||||
inventory: Inventory = self.entity.inventory
|
||||
engine: Engine = self.entity.floor_map.engine
|
||||
|
||||
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)
|
||||
|
||||
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}"
|
||||
|
||||
engine.message_log.add_message(msg, color=colors.terminal_light)
|
||||
|
||||
return True
|
@ -2,6 +2,7 @@ from entity import Entity
|
||||
from ai import BaseAI, AggressiveWhenSeen
|
||||
from stats import Stats
|
||||
from inventory import Inventory
|
||||
from useable import PotionOfHealing
|
||||
|
||||
#player and utils
|
||||
player = Entity(
|
||||
@ -30,8 +31,15 @@ gobbo_red = Entity(
|
||||
name = "Red Gobbo",
|
||||
walkable = False,
|
||||
ai_class = AggressiveWhenSeen,
|
||||
stats = Stats(hp = 5, attack = 1, defense = 0), #this guy can't catch a break
|
||||
stats = Stats(hp = 1, attack = 2, defense = 0), #this guy can't catch a break
|
||||
)
|
||||
|
||||
#items - conumables
|
||||
#TODO: potion of healing entity
|
||||
potion_of_healing = Entity(
|
||||
char = "!",
|
||||
color = (0, 0, 255),
|
||||
name = "Potion of Healing",
|
||||
walkable = True,
|
||||
useable=PotionOfHealing(current_stack=1, maximum_stack=255, consumable=True),
|
||||
)
|
||||
|
||||
|
@ -11,6 +11,8 @@ from actions import (
|
||||
WaitAction,
|
||||
PickupAction,
|
||||
DropAction,
|
||||
DropPartialStackAction,
|
||||
UsageAction,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -245,9 +247,12 @@ class InventoryViewer(EventHandler):
|
||||
#render the cursor & inventory contents
|
||||
offset: int = 0
|
||||
for item in self.entity.inventory.contents:
|
||||
msg = item.name
|
||||
if item.useable.current_stack > 1:
|
||||
msg = f"{msg} x{item.useable.current_stack}"
|
||||
inner_console.print(
|
||||
4, 2 + offset,
|
||||
string = item.name,
|
||||
string = msg,
|
||||
fg=colors.terminal_light, bg=colors.black,
|
||||
)
|
||||
offset += 1
|
||||
@ -280,10 +285,20 @@ class InventoryViewer(EventHandler):
|
||||
#drop an item form this entity's inventory
|
||||
item: Entity = self.entity.inventory.access(self.cursor)
|
||||
|
||||
#defaults
|
||||
options = ["Use", "Drop", "Back"]
|
||||
callback = lambda x: self.default_selector_callback(x)
|
||||
|
||||
#different options for different situations
|
||||
if item.useable.current_stack > 1:
|
||||
options = ["Use", "Drop 1", "Drop All", "Back"]
|
||||
callback = lambda x: self.stack_selector_callback(x)
|
||||
|
||||
#TODO: drop 1, drop all for stacks
|
||||
self.engine.event_handler = OptionSelector(self.engine, self,
|
||||
title=f"Drop The {item.name}?",
|
||||
options=["Yes", "No"],
|
||||
callback=lambda x: self.selector_callback(x)
|
||||
title=item.name,
|
||||
options=options,
|
||||
callback=callback
|
||||
)
|
||||
|
||||
#TODO: hotkeys via a config
|
||||
@ -297,22 +312,49 @@ class InventoryViewer(EventHandler):
|
||||
self.engine.event_handler = self.parent_handler
|
||||
|
||||
#utils
|
||||
def selector_callback(self, answer: int) -> Optional[BaseAction]:
|
||||
#TODO: insert a sub-selection box to choose what to do with this item
|
||||
if answer == 0: #TODO: Use, Drop, Back
|
||||
def default_selector_callback(self, selected: int) -> Optional[BaseAction]:
|
||||
if selected == 0: #Use
|
||||
return self.use()
|
||||
elif selected == 1: #Drop
|
||||
return self.drop()
|
||||
elif selected == 2: #Back
|
||||
return None #the selector does the change
|
||||
|
||||
def stack_selector_callback(self, selected: int) -> Optional[BaseAction]:
|
||||
if selected == 0: #Use
|
||||
return self.use()
|
||||
elif selected == 1: #Drop 1
|
||||
return self.drop_partial_stack(1)
|
||||
elif selected == 2: #Drop all
|
||||
return self.drop()
|
||||
elif selected == 3: #Back
|
||||
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."""
|
||||
if self.length > 0:
|
||||
index = self.cursor
|
||||
|
||||
return UsageAction(self.entity, index, self.entity, lambda x: self.adjust_length(x))
|
||||
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
#bounds
|
||||
self.length -= 1
|
||||
if self.cursor >= self.length:
|
||||
self.cursor = self.length - 1
|
||||
|
||||
return DropAction(self.entity, index)
|
||||
return DropAction(self.entity, index, lambda x: self.adjust_length(x))
|
||||
|
||||
def adjust_length(self, amount: int):
|
||||
self.length += amount
|
||||
if self.cursor >= self.length:
|
||||
self.cursor = self.length - 1
|
||||
|
||||
|
||||
#generic tools
|
||||
|
@ -11,11 +11,16 @@ class Inventory:
|
||||
def __init__(self, contents: List[Entity] = []):
|
||||
self._contents = contents
|
||||
|
||||
def insert(self, entity: Entity) -> bool:
|
||||
if entity in self._contents:
|
||||
def insert(self, item: Entity) -> bool:
|
||||
if item in self._contents:
|
||||
return False
|
||||
|
||||
self._contents.append(entity)
|
||||
#check for stacking
|
||||
if item.useable.maximum_stack > 0:
|
||||
if self.try_stack_merge(item):
|
||||
return True
|
||||
|
||||
self._contents.append(item)
|
||||
return True
|
||||
|
||||
def access(self, index: int) -> Optional[Entity]:
|
||||
@ -29,9 +34,25 @@ class Inventory:
|
||||
return None
|
||||
else:
|
||||
return self._contents.pop(index)
|
||||
|
||||
def discard(self, index: int) -> None:
|
||||
if index < 0 or index >= len(self._contents):
|
||||
pass
|
||||
else:
|
||||
self._contents.pop(index)
|
||||
|
||||
@property
|
||||
def contents(self) -> List[Entity]:
|
||||
return self._contents
|
||||
|
||||
#utils
|
||||
def try_stack_merge(self, new_item: Entity):
|
||||
for item in self._contents:
|
||||
if item.useable.is_stack_mergable(new_item.useable):
|
||||
#TODO: add a callback in the entity if other components need to be tweaked down the road
|
||||
item.useable.current_stack += new_item.useable.current_stack
|
||||
new_item.useable.current_stack = 0 #just in case
|
||||
return True
|
||||
return False
|
||||
|
||||
#TODO: items need a weight, inventory needs a max capacity
|
@ -30,7 +30,7 @@ def main() -> None:
|
||||
|
||||
engine = Engine(
|
||||
#is created externally, because
|
||||
floor_map = generate_floor_map(map_width, map_height, room_width_max=12, room_height_max=12),
|
||||
floor_map = generate_floor_map(map_width, map_height, room_width_max=12, room_height_max=12, room_items_max=4),
|
||||
ui_height = ui_height,
|
||||
|
||||
initial_log= [
|
||||
|
@ -50,13 +50,19 @@ class RectangularRoom:
|
||||
self.y2 >= other.y1
|
||||
)
|
||||
|
||||
def spawn_monsters(floor_map: FloorMap, room: RectangularRoom, room_monsters_max: int) -> None:
|
||||
monster_count = random.randint(0, room_monsters_max)
|
||||
def spawn_monsters(floor_map: FloorMap, room: RectangularRoom, room_monsters_max: int, floor_monsters_max: int) -> None:
|
||||
if "monster_count" not in floor_map.procgen_cache:
|
||||
floor_map.procgen_cache["monster_count"] = 0
|
||||
else:
|
||||
if floor_monsters_max >= 0 and floor_map.procgen_cache["monster_count"] >= floor_monsters_max:
|
||||
return #cap the monsters total
|
||||
|
||||
#There can only be one
|
||||
if "gobbo_red" not in floor_map.procgen_cache:
|
||||
floor_map.procgen_cache["gobbo_red"] = False
|
||||
|
||||
monster_count = random.randint(0, room_monsters_max)
|
||||
|
||||
for i in range(monster_count):
|
||||
#admittedly weird layout here, because player isn't in the entities list yet
|
||||
x, y = floor_map.player.x, floor_map.player.y
|
||||
@ -65,13 +71,32 @@ def spawn_monsters(floor_map: FloorMap, room: RectangularRoom, room_monsters_max
|
||||
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 any(entity.x == x and entity.y == y and not entity.walkable for entity in floor_map.entities):
|
||||
#there's never more than one red gobbo, but there can be none at all
|
||||
if not floor_map.procgen_cache["gobbo_red"] and random.random() < 0.2:
|
||||
floor_map.procgen_cache["gobbo_red"] = True
|
||||
entity_types.gobbo_red.spawn(x, y, floor_map)
|
||||
else:
|
||||
entity_types.gobbo.spawn(x, y, floor_map)
|
||||
floor_map.procgen_cache["monster_count"] += 1
|
||||
|
||||
def spawn_items(floor_map: FloorMap, room: RectangularRoom, room_items_max: int, floor_items_max: int) -> None:
|
||||
item_count = random.randint(0, room_items_max)
|
||||
|
||||
if "item_count" not in floor_map.procgen_cache:
|
||||
floor_map.procgen_cache["item_count"] = 0
|
||||
else:
|
||||
if floor_items_max >= 0 and floor_map.procgen_cache["item_count"] >= floor_items_max:
|
||||
return #cap the item total
|
||||
|
||||
for i in range(item_count):
|
||||
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 (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)
|
||||
floor_map.procgen_cache["item_count"] += 1
|
||||
|
||||
#generators
|
||||
def generate_floor_map(
|
||||
@ -82,9 +107,12 @@ def generate_floor_map(
|
||||
room_width_min: int = 6,
|
||||
room_height_min: int = 6,
|
||||
room_count_max: int = 20,
|
||||
room_monsters_max: int = 2
|
||||
room_monsters_max: int = 2,
|
||||
room_items_max: int = 1,
|
||||
floor_monsters_max: int = -1,
|
||||
floor_items_max: int = -1,
|
||||
) -> FloorMap:
|
||||
#simplistic floor generator
|
||||
#simplistic floor generator - it'll get rewritten eventually
|
||||
floor_map: FloorMap = FloorMap(map_width, map_height)
|
||||
|
||||
rooms: List[RectangularRoom] = []
|
||||
@ -110,7 +138,9 @@ def generate_floor_map(
|
||||
for x, y in make_corridor(rooms[-1].center, new_room.center):
|
||||
floor_map.tiles[x, y] = tile_types.floor
|
||||
|
||||
spawn_monsters(floor_map, new_room, room_monsters_max)
|
||||
spawn_monsters(floor_map, new_room, room_monsters_max, floor_monsters_max)
|
||||
|
||||
spawn_items(floor_map, new_room, room_items_max, floor_items_max)
|
||||
|
||||
rooms.append(new_room)
|
||||
|
||||
|
@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
|
||||
import colors
|
||||
|
||||
from event_handlers import GameOverHandler
|
||||
from useable import BaseUseable
|
||||
from useable import Unuseable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from engine import Engine
|
||||
@ -47,9 +47,10 @@ class Stats:
|
||||
engine.message_log.add_message(f"The {self.entity.name} died", colors.yellow)
|
||||
|
||||
#transform into a dead body
|
||||
#TODO: dummied in a "usable" to let dead objects be treated like items
|
||||
self.entity.char = "%"
|
||||
self.entity.color = (191, 0, 0)
|
||||
self.entity.walkable = True
|
||||
self.entity.ai = None #TODO: Could decay over time
|
||||
self.entity.useable = BaseUseable() #TODO: dummied in a "usable" to let dead objects be treated like items
|
||||
self.entity.useable = Unuseable()
|
||||
self.entity.name = f"Dead {self.entity.name}"
|
||||
|
@ -1,31 +1,97 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from actions import BaseAction
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from stats import Stats
|
||||
|
||||
class BaseUseable:
|
||||
"""Base type for useable items, with various utilities"""
|
||||
current_stack: int
|
||||
maximum_stack: int
|
||||
consumable: bool
|
||||
|
||||
def apply(self, stats: Stats) -> BaseAction:
|
||||
"""Use this item's effects"""
|
||||
def __init__(self, *, current_stack: int = 1, maximum_stack: int = -1, consumable: bool = False):
|
||||
self.current_stack = current_stack
|
||||
self.maximum_stack = maximum_stack
|
||||
self.consumable = consumable
|
||||
|
||||
def apply(self, stats: Stats) -> bool:
|
||||
"""
|
||||
Use this item's effects.
|
||||
|
||||
Returns `True` if the item's state changed.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
||||
"""
|
||||
May return a string to display to the user.
|
||||
|
||||
class PotionOfHealing(BaseUseable): #TODO: Finish the potion of healing
|
||||
"""Restore a specified amount of health to the given Stats object"""
|
||||
amount: int
|
||||
`appearance` is what the item looks like, and can be substituted into the result.
|
||||
"""
|
||||
return None #default
|
||||
|
||||
#utils
|
||||
def reduce_stack(self, amount: int = 1) -> bool:
|
||||
"""
|
||||
Reduce the size of a stack by an amount.
|
||||
|
||||
def __init__(self, amount: int):
|
||||
self.amount = amount
|
||||
Returns `True` if this item should be deleted.
|
||||
"""
|
||||
if self.maximum_stack > 0:
|
||||
self.current_stack -= amount
|
||||
return self.current_stack <= 0
|
||||
return self.consumable
|
||||
|
||||
def is_stack_empty(self) -> bool:
|
||||
return self.consumable and self.maximum_stack > 0 and self.current_stack <= 0
|
||||
|
||||
def is_stack_mergable(self, other: BaseUseable) -> bool:
|
||||
"""
|
||||
If this returns `True`, this instance can be merged with the other instance.
|
||||
"""
|
||||
if self.__class__ is not other.__class__:
|
||||
return False
|
||||
|
||||
max_stack = max(self.maximum_stack, other.maximum_stack)
|
||||
return self.current_stack + other.current_stack <= max_stack
|
||||
|
||||
|
||||
class Unuseable(BaseUseable):
|
||||
"""A placeholder Useable for dead entities."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__() #enforce defaults
|
||||
|
||||
def apply(self, stats: Stats) -> bool:
|
||||
return None
|
||||
|
||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
||||
return f"This {appearance} is utterly useless."
|
||||
|
||||
|
||||
class PotionOfHealing(BaseUseable):
|
||||
"""Restores 4d4 health when applied."""
|
||||
__last_roll: int = -1
|
||||
|
||||
def apply(self, stats: Stats) -> bool:
|
||||
self.__last_roll = roll_dice(4, 4)
|
||||
stats.current_hp += self.__last_roll
|
||||
return self.reduce_stack()
|
||||
|
||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
||||
return f"You restored {self.__last_roll} health."
|
||||
|
||||
def apply(self, stats: Stats) -> BaseAction:
|
||||
"""Use this item's effects"""
|
||||
raise NotImplementedError()
|
||||
|
||||
# NOTE: NetHack's version
|
||||
# Healing: 8d4 | 6d4 | 4d4. If the result is above MaxHP, MaxHP is incrased by 1 | 1 | 0.
|
||||
# Extra Healing: 8d8 | 6d8 | 4d8. If the result is above MaxHP, MaxHP is incrased by 5 | 2 | 0.
|
||||
# Full Healing: 400 | 400 | 400. If the result is above MaxHP, MaxHP is incrased by 8 | 4 | 0.
|
||||
|
||||
#TODO: move this into a different file
|
||||
import random
|
||||
def roll_dice(number: int, sides: int) -> int:
|
||||
total: int = 0
|
||||
for i in range(number):
|
||||
total += random.randint(1, sides)
|
||||
return total
|
||||
|
Loading…
x
Reference in New Issue
Block a user