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
|
import colors
|
||||||
from floor_map import FloorMap
|
from floor_map import FloorMap
|
||||||
from inventory import Inventory
|
from inventory import Inventory
|
||||||
|
from useable import BaseUseable
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from engine import Engine
|
from engine import Engine
|
||||||
@ -132,9 +133,13 @@ class PickupAction(BaseAction):
|
|||||||
if len(item_stack) == 0:
|
if len(item_stack) == 0:
|
||||||
return False
|
return False
|
||||||
elif len(item_stack) == 1:
|
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])
|
floor_map.entities.remove(item_stack[0])
|
||||||
self.entity.inventory.insert(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:
|
else:
|
||||||
from event_handlers import OptionSelector #circular imports are a pain
|
from event_handlers import OptionSelector #circular imports are a pain
|
||||||
|
|
||||||
@ -155,18 +160,24 @@ class PickupAction(BaseAction):
|
|||||||
|
|
||||||
#utils
|
#utils
|
||||||
def pickup_callback(self, engine: Engine, floor_map: FloorMap, entity: Entity, item: Entity) -> None:
|
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)
|
floor_map.entities.remove(item)
|
||||||
entity.inventory.insert(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):
|
class DropAction(BaseAction):
|
||||||
"""Drop an item from an entity's inventory at the entity's location"""
|
"""Drop an item from an entity's inventory at the entity's location"""
|
||||||
index: int
|
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__"""
|
"""override the base __init__"""
|
||||||
super().__init__(entity)
|
super().__init__(entity)
|
||||||
self.index = index
|
self.index = index
|
||||||
|
self.display_callback = display_callback
|
||||||
|
|
||||||
def perform(self) -> bool:
|
def perform(self) -> bool:
|
||||||
x = self.entity.x
|
x = self.entity.x
|
||||||
@ -183,6 +194,81 @@ class DropAction(BaseAction):
|
|||||||
|
|
||||||
floor_map.entities.add(item)
|
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)
|
engine.message_log.add_message(f"you dropped a(n) {item.name}", color=colors.terminal_light)
|
||||||
|
|
||||||
return True
|
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 ai import BaseAI, AggressiveWhenSeen
|
||||||
from stats import Stats
|
from stats import Stats
|
||||||
from inventory import Inventory
|
from inventory import Inventory
|
||||||
|
from useable import PotionOfHealing
|
||||||
|
|
||||||
#player and utils
|
#player and utils
|
||||||
player = Entity(
|
player = Entity(
|
||||||
@ -30,8 +31,15 @@ gobbo_red = Entity(
|
|||||||
name = "Red Gobbo",
|
name = "Red Gobbo",
|
||||||
walkable = False,
|
walkable = False,
|
||||||
ai_class = AggressiveWhenSeen,
|
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
|
#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,
|
WaitAction,
|
||||||
PickupAction,
|
PickupAction,
|
||||||
DropAction,
|
DropAction,
|
||||||
|
DropPartialStackAction,
|
||||||
|
UsageAction,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -245,9 +247,12 @@ class InventoryViewer(EventHandler):
|
|||||||
#render the cursor & inventory contents
|
#render the cursor & inventory contents
|
||||||
offset: int = 0
|
offset: int = 0
|
||||||
for item in self.entity.inventory.contents:
|
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(
|
inner_console.print(
|
||||||
4, 2 + offset,
|
4, 2 + offset,
|
||||||
string = item.name,
|
string = msg,
|
||||||
fg=colors.terminal_light, bg=colors.black,
|
fg=colors.terminal_light, bg=colors.black,
|
||||||
)
|
)
|
||||||
offset += 1
|
offset += 1
|
||||||
@ -280,10 +285,20 @@ class InventoryViewer(EventHandler):
|
|||||||
#drop an item form this entity's inventory
|
#drop an item form this entity's inventory
|
||||||
item: Entity = self.entity.inventory.access(self.cursor)
|
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,
|
self.engine.event_handler = OptionSelector(self.engine, self,
|
||||||
title=f"Drop The {item.name}?",
|
title=item.name,
|
||||||
options=["Yes", "No"],
|
options=options,
|
||||||
callback=lambda x: self.selector_callback(x)
|
callback=callback
|
||||||
)
|
)
|
||||||
|
|
||||||
#TODO: hotkeys via a config
|
#TODO: hotkeys via a config
|
||||||
@ -297,23 +312,50 @@ class InventoryViewer(EventHandler):
|
|||||||
self.engine.event_handler = self.parent_handler
|
self.engine.event_handler = self.parent_handler
|
||||||
|
|
||||||
#utils
|
#utils
|
||||||
def selector_callback(self, answer: int) -> Optional[BaseAction]:
|
def default_selector_callback(self, selected: int) -> Optional[BaseAction]:
|
||||||
#TODO: insert a sub-selection box to choose what to do with this item
|
if selected == 0: #Use
|
||||||
if answer == 0: #TODO: Use, Drop, Back
|
return self.use()
|
||||||
|
elif selected == 1: #Drop
|
||||||
return self.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]:
|
def drop(self) -> Optional[BaseAction]:
|
||||||
"""Drop the item at the cursor's position, and adjust the cursor if needed."""
|
"""Drop the item at the cursor's position, and adjust the cursor if needed."""
|
||||||
if self.length > 0:
|
if self.length > 0:
|
||||||
index = self.cursor
|
index = self.cursor
|
||||||
|
|
||||||
#bounds
|
return DropAction(self.entity, index, lambda x: self.adjust_length(x))
|
||||||
self.length -= 1
|
|
||||||
|
def adjust_length(self, amount: int):
|
||||||
|
self.length += amount
|
||||||
if self.cursor >= self.length:
|
if self.cursor >= self.length:
|
||||||
self.cursor = self.length - 1
|
self.cursor = self.length - 1
|
||||||
|
|
||||||
return DropAction(self.entity, index)
|
|
||||||
|
|
||||||
|
|
||||||
#generic tools
|
#generic tools
|
||||||
class OptionSelector(EventHandler):
|
class OptionSelector(EventHandler):
|
||||||
|
@ -11,11 +11,16 @@ class Inventory:
|
|||||||
def __init__(self, contents: List[Entity] = []):
|
def __init__(self, contents: List[Entity] = []):
|
||||||
self._contents = contents
|
self._contents = contents
|
||||||
|
|
||||||
def insert(self, entity: Entity) -> bool:
|
def insert(self, item: Entity) -> bool:
|
||||||
if entity in self._contents:
|
if item in self._contents:
|
||||||
return False
|
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
|
return True
|
||||||
|
|
||||||
def access(self, index: int) -> Optional[Entity]:
|
def access(self, index: int) -> Optional[Entity]:
|
||||||
@ -30,8 +35,24 @@ class Inventory:
|
|||||||
else:
|
else:
|
||||||
return self._contents.pop(index)
|
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
|
@property
|
||||||
def contents(self) -> List[Entity]:
|
def contents(self) -> List[Entity]:
|
||||||
return self._contents
|
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
|
#TODO: items need a weight, inventory needs a max capacity
|
@ -30,7 +30,7 @@ def main() -> None:
|
|||||||
|
|
||||||
engine = Engine(
|
engine = Engine(
|
||||||
#is created externally, because
|
#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,
|
ui_height = ui_height,
|
||||||
|
|
||||||
initial_log= [
|
initial_log= [
|
||||||
|
@ -50,13 +50,19 @@ class RectangularRoom:
|
|||||||
self.y2 >= other.y1
|
self.y2 >= other.y1
|
||||||
)
|
)
|
||||||
|
|
||||||
def spawn_monsters(floor_map: FloorMap, room: RectangularRoom, room_monsters_max: int) -> None:
|
def spawn_monsters(floor_map: FloorMap, room: RectangularRoom, room_monsters_max: int, floor_monsters_max: int) -> None:
|
||||||
monster_count = random.randint(0, room_monsters_max)
|
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
|
#There can only be one
|
||||||
if "gobbo_red" not in floor_map.procgen_cache:
|
if "gobbo_red" not in floor_map.procgen_cache:
|
||||||
floor_map.procgen_cache["gobbo_red"] = False
|
floor_map.procgen_cache["gobbo_red"] = False
|
||||||
|
|
||||||
|
monster_count = random.randint(0, room_monsters_max)
|
||||||
|
|
||||||
for i in range(monster_count):
|
for i in range(monster_count):
|
||||||
#admittedly weird layout here, because player isn't in the entities list yet
|
#admittedly weird layout here, because player isn't in the entities list yet
|
||||||
x, y = floor_map.player.x, floor_map.player.y
|
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)
|
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||||||
|
|
||||||
#if there's no entity at that position
|
#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
|
#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:
|
if not floor_map.procgen_cache["gobbo_red"] and random.random() < 0.2:
|
||||||
floor_map.procgen_cache["gobbo_red"] = True
|
floor_map.procgen_cache["gobbo_red"] = True
|
||||||
entity_types.gobbo_red.spawn(x, y, floor_map)
|
entity_types.gobbo_red.spawn(x, y, floor_map)
|
||||||
else:
|
else:
|
||||||
entity_types.gobbo.spawn(x, y, floor_map)
|
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
|
#generators
|
||||||
def generate_floor_map(
|
def generate_floor_map(
|
||||||
@ -82,9 +107,12 @@ def generate_floor_map(
|
|||||||
room_width_min: int = 6,
|
room_width_min: int = 6,
|
||||||
room_height_min: int = 6,
|
room_height_min: int = 6,
|
||||||
room_count_max: int = 20,
|
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:
|
) -> FloorMap:
|
||||||
#simplistic floor generator
|
#simplistic floor generator - it'll get rewritten eventually
|
||||||
floor_map: FloorMap = FloorMap(map_width, map_height)
|
floor_map: FloorMap = FloorMap(map_width, map_height)
|
||||||
|
|
||||||
rooms: List[RectangularRoom] = []
|
rooms: List[RectangularRoom] = []
|
||||||
@ -110,7 +138,9 @@ def generate_floor_map(
|
|||||||
for x, y in make_corridor(rooms[-1].center, new_room.center):
|
for x, y in make_corridor(rooms[-1].center, new_room.center):
|
||||||
floor_map.tiles[x, y] = tile_types.floor
|
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)
|
rooms.append(new_room)
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
|
|||||||
import colors
|
import colors
|
||||||
|
|
||||||
from event_handlers import GameOverHandler
|
from event_handlers import GameOverHandler
|
||||||
from useable import BaseUseable
|
from useable import Unuseable
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from engine import Engine
|
from engine import Engine
|
||||||
@ -47,9 +47,10 @@ class Stats:
|
|||||||
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.yellow)
|
||||||
|
|
||||||
#transform into a dead body
|
#transform into a dead body
|
||||||
|
#TODO: dummied in a "usable" to let dead objects be treated like items
|
||||||
self.entity.char = "%"
|
self.entity.char = "%"
|
||||||
self.entity.color = (191, 0, 0)
|
self.entity.color = (191, 0, 0)
|
||||||
self.entity.walkable = True
|
self.entity.walkable = True
|
||||||
self.entity.ai = None #TODO: Could decay over time
|
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}"
|
self.entity.name = f"Dead {self.entity.name}"
|
||||||
|
@ -1,31 +1,97 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from actions import BaseAction
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from stats import Stats
|
from stats import Stats
|
||||||
|
|
||||||
class BaseUseable:
|
class BaseUseable:
|
||||||
"""Base type for useable items, with various utilities"""
|
"""Base type for useable items, with various utilities"""
|
||||||
|
current_stack: int
|
||||||
|
maximum_stack: int
|
||||||
|
consumable: bool
|
||||||
|
|
||||||
def apply(self, stats: Stats) -> BaseAction:
|
def __init__(self, *, current_stack: int = 1, maximum_stack: int = -1, consumable: bool = False):
|
||||||
"""Use this item's effects"""
|
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()
|
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
|
`appearance` is what the item looks like, and can be substituted into the result.
|
||||||
"""Restore a specified amount of health to the given Stats object"""
|
"""
|
||||||
amount: int
|
return None #default
|
||||||
|
|
||||||
def __init__(self, amount: int):
|
#utils
|
||||||
self.amount = amount
|
def reduce_stack(self, amount: int = 1) -> bool:
|
||||||
|
"""
|
||||||
|
Reduce the size of a stack by an 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
|
# NOTE: NetHack's version
|
||||||
# Healing: 8d4 | 6d4 | 4d4. If the result is above MaxHP, MaxHP is incrased by 1 | 1 | 0.
|
# 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.
|
# 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.
|
# 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