Added confusion, fireball, and various utils
While the scrolls themselves won't be around forever, the tools used to make them work will be.
This commit is contained in:
parent
1722681823
commit
6a42bb8c59
@ -13,7 +13,7 @@ https://pyinstaller.org/en/stable/
|
|||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
|
|
||||||
#useful pre-commit hook
|
#useful pre-commit hook
|
||||||
lint_results=$(echo "$(grep -Pro '[\t]+$' */*.py)")
|
lint_results="$(echo "$(grep -Pro '[\t]+$' */*.py)") $(echo "$(grep -Pro 'usable' */*.py)")"
|
||||||
lint_errors=$(echo "$lint_results" | wc -w)
|
lint_errors=$(echo "$lint_results" | wc -w)
|
||||||
if [ $lint_errors -gt 0 ]; then
|
if [ $lint_errors -gt 0 ]; then
|
||||||
echo "pre-commit lint errors found: $lint_errors"
|
echo "pre-commit lint errors found: $lint_errors"
|
||||||
@ -72,6 +72,7 @@ To-wound:
|
|||||||
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?
|
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?
|
||||||
Should the selected dimension be known at the start of a run?
|
Should the selected dimension be known at the start of a run?
|
||||||
More of a certain dungeon feature in some runs i.e. too many fountains run?
|
More of a certain dungeon feature in some runs i.e. too many fountains run?
|
||||||
|
Inventory should show item weights
|
||||||
|
|
||||||
## Healing Items
|
## Healing Items
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ from useable import BaseUseable
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from engine import Engine
|
from engine import Engine
|
||||||
from event_handlers import OptionSelector, TileSelector
|
|
||||||
from entity import Entity
|
from entity import Entity
|
||||||
|
|
||||||
class BaseAction:
|
class BaseAction:
|
||||||
@ -142,6 +141,8 @@ class PickupAction(BaseAction):
|
|||||||
self.entity.inventory.insert(item)
|
self.entity.inventory.insert(item)
|
||||||
engine.message_log.add_message(msg, color=colors.terminal_light)
|
engine.message_log.add_message(msg, color=colors.terminal_light)
|
||||||
else:
|
else:
|
||||||
|
from event_handlers import OptionSelector
|
||||||
|
|
||||||
#build an options list
|
#build an options list
|
||||||
options: List[str] = []
|
options: List[str] = []
|
||||||
for item in item_pile:
|
for item in item_pile:
|
||||||
@ -263,29 +264,29 @@ class UsageAction(BaseAction):
|
|||||||
engine: Engine = self.entity.floor_map.engine
|
engine: Engine = self.entity.floor_map.engine
|
||||||
|
|
||||||
item: Entity = inventory.access(self.index)
|
item: Entity = inventory.access(self.index)
|
||||||
usable: BaseUseable = item.useable
|
useable: BaseUseable = item.useable
|
||||||
|
|
||||||
#TODO: also check visibility?
|
#TODO: also check visibility?
|
||||||
|
|
||||||
#check range
|
#check range
|
||||||
distance = self.entity.get_distance_to(self.target.x, self.target.y)
|
distance = self.entity.get_distance_to(self.target.x, self.target.y)
|
||||||
|
|
||||||
if usable.minimum_range > distance:
|
if useable.minimum_range >= 0 and useable.minimum_range > distance:
|
||||||
engine.message_log.add_message("You're too close to use this!", color=colors.terminal_light)
|
engine.message_log.add_message("You're too close to use this!", color=colors.terminal_light)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif usable.maximum_range < distance:
|
elif useable.maximum_range >= 0 and useable.maximum_range < distance:
|
||||||
engine.message_log.add_message("You're too far to use this!", color=colors.terminal_light)
|
engine.message_log.add_message("You're too far to use this!", color=colors.terminal_light)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
elif usable.apply(self.target.stats) and usable.is_stack_empty():
|
elif useable.apply(self.target) and useable.is_stack_empty():
|
||||||
#remove the item from the inventory
|
#remove the item from the inventory
|
||||||
inventory.discard(self.index)
|
inventory.discard(self.index)
|
||||||
|
|
||||||
if self.display_callback:
|
if self.display_callback:
|
||||||
self.display_callback(-1)
|
self.display_callback(-1)
|
||||||
|
|
||||||
msg: str = usable.get_used_msg(item.name)
|
msg: str = useable.get_used_msg()
|
||||||
if not msg:
|
if not msg:
|
||||||
msg = f"What is a(n) {item.name}?"
|
msg = f"What is a(n) {item.name}?"
|
||||||
|
|
||||||
|
69
source/ai.py
69
source/ai.py
@ -1,26 +1,37 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import List, Tuple, TYPE_CHECKING
|
from typing import List, Tuple, TYPE_CHECKING
|
||||||
|
import random
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import tcod
|
import tcod
|
||||||
|
|
||||||
from actions import BaseAction, MeleeAction, MovementAction, WaitAction
|
from actions import (
|
||||||
|
BaseAction,
|
||||||
|
MeleeAction,
|
||||||
|
MovementAction,
|
||||||
|
BumpAction,
|
||||||
|
WaitAction,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from entity import Entity
|
from entity import Entity
|
||||||
|
|
||||||
class BaseAI:
|
class BaseAI:
|
||||||
"""Base type for monster AI, with various utilities"""
|
"""Base type for creature AI, with various utilities."""
|
||||||
entity: Entity
|
entity: Entity
|
||||||
|
|
||||||
def __init__(self, entity):
|
def __init__(self, entity: Entity):
|
||||||
|
#grab the entity
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
|
|
||||||
|
#utilities
|
||||||
self.path: List[Tuple[int, int]] = []
|
self.path: List[Tuple[int, int]] = []
|
||||||
|
|
||||||
def process(self) -> BaseAction:
|
def process(self) -> BaseAction:
|
||||||
"""Decides what action to take"""
|
"""Allow the AI to think."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
#utils
|
||||||
def generate_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
|
def generate_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
|
||||||
#copy the walkables
|
#copy the walkables
|
||||||
cost = np.array(self.entity.floor_map.tiles["walkable"], dtype=np.int8)
|
cost = np.array(self.entity.floor_map.tiles["walkable"], dtype=np.int8)
|
||||||
@ -44,16 +55,18 @@ class BaseAI:
|
|||||||
|
|
||||||
class AggressiveWhenSeen(BaseAI):
|
class AggressiveWhenSeen(BaseAI):
|
||||||
"""
|
"""
|
||||||
If the player can seem me, try to approach and attack.
|
If I'm close enough to the player, attack.
|
||||||
Otherwise, idle.
|
If I see where the player is, approach that point.
|
||||||
|
If I know where the player has been, approach that point.
|
||||||
|
If all else fails, idle.
|
||||||
"""
|
"""
|
||||||
def process(self) -> BaseAction:
|
def process(self) -> BaseAction:
|
||||||
target = self.entity.floor_map.player
|
target = self.entity.floor_map.player #TODO: friendly fire?
|
||||||
xdir = target.x - self.entity.x
|
xdir = target.x - self.entity.x
|
||||||
ydir = target.y - self.entity.y
|
ydir = target.y - self.entity.y
|
||||||
distance = max(abs(xdir), abs(ydir))
|
distance = max(abs(xdir), abs(ydir))
|
||||||
|
|
||||||
#if the player can see me
|
#if the player can see me, create or update my path
|
||||||
if self.entity.floor_map.visible[self.entity.x, self.entity.y]:
|
if self.entity.floor_map.visible[self.entity.x, self.entity.y]:
|
||||||
#if I'm close enough to attack
|
#if I'm close enough to attack
|
||||||
if distance <= 1:
|
if distance <= 1:
|
||||||
@ -61,7 +74,7 @@ class AggressiveWhenSeen(BaseAI):
|
|||||||
|
|
||||||
self.path = self.generate_path_to(target.x, target.y)
|
self.path = self.generate_path_to(target.x, target.y)
|
||||||
|
|
||||||
#if I have a path to follow
|
#if I have a path to follow, regardless of current visability
|
||||||
if self.path:
|
if self.path:
|
||||||
dest_x, dest_y = self.path.pop(0)
|
dest_x, dest_y = self.path.pop(0)
|
||||||
return MovementAction(
|
return MovementAction(
|
||||||
@ -72,3 +85,41 @@ class AggressiveWhenSeen(BaseAI):
|
|||||||
|
|
||||||
#idle
|
#idle
|
||||||
return WaitAction(self.entity)
|
return WaitAction(self.entity)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfusedAI(BaseAI):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
parent_ai: BaseAI
|
||||||
|
duration: int
|
||||||
|
|
||||||
|
def __init__(self, entity: Entity, duration: int):
|
||||||
|
super().__init__(entity)
|
||||||
|
self.parent_ai = entity.ai
|
||||||
|
self.duration = duration
|
||||||
|
|
||||||
|
def process(self) -> BaseAction:
|
||||||
|
if self.duration <= 0:
|
||||||
|
self.entity.ai = self.parent_ai
|
||||||
|
return self.entity.ai.process()
|
||||||
|
|
||||||
|
self.duration -= 1
|
||||||
|
|
||||||
|
xdir, ydir = random.choice(
|
||||||
|
[
|
||||||
|
[ -1, -1],
|
||||||
|
[ -1, 0],
|
||||||
|
[ -1, 1],
|
||||||
|
|
||||||
|
[ 0, -1],
|
||||||
|
#[ 0, 0],
|
||||||
|
[ 0, 1],
|
||||||
|
|
||||||
|
[ 1, -1],
|
||||||
|
[ 1, 0],
|
||||||
|
[ 1, 1],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
#Oh, this allows for friendly fire!
|
||||||
|
return BumpAction(self.entity, xdir, ydir)
|
@ -21,6 +21,7 @@ purple = (0x80, 0x00, 0x80) #identical to magenta
|
|||||||
#CSS extended colors (incomplete selection, may be expanded later)
|
#CSS extended colors (incomplete selection, may be expanded later)
|
||||||
pink = (0xFF, 0xC0, 0xCB)
|
pink = (0xFF, 0xC0, 0xCB)
|
||||||
orange = (0xFF, 0xA5, 0x00)
|
orange = (0xFF, 0xA5, 0x00)
|
||||||
|
orange_red = (0xFF, 0x45, 0x00)
|
||||||
deep_sky_blue = (0x00, 0xBF, 0xFF)
|
deep_sky_blue = (0x00, 0xBF, 0xFF)
|
||||||
navajo_white = (0xFF, 0xDE, 0xAD) #misnomer
|
navajo_white = (0xFF, 0xDE, 0xAD) #misnomer
|
||||||
goldenrod = (0xDA, 0xA5, 0x20)
|
goldenrod = (0xDA, 0xA5, 0x20)
|
||||||
|
@ -59,6 +59,7 @@ class Entity:
|
|||||||
|
|
||||||
if useable:
|
if useable:
|
||||||
self.useable = useable
|
self.useable = useable
|
||||||
|
self.useable.entity = self
|
||||||
|
|
||||||
#generic entity stuff
|
#generic entity stuff
|
||||||
def spawn(self, x: int, y: int, floor_map: FloorMap):
|
def spawn(self, x: int, y: int, floor_map: FloorMap):
|
||||||
|
@ -38,7 +38,7 @@ gobbo_red = Entity(
|
|||||||
#items - conumables
|
#items - conumables
|
||||||
potion_of_healing = Entity(
|
potion_of_healing = Entity(
|
||||||
char = "!",
|
char = "!",
|
||||||
color = colors.deep_sky_blue,
|
color = colors.green,
|
||||||
name = "Potion of Healing",
|
name = "Potion of Healing",
|
||||||
walkable = True,
|
walkable = True,
|
||||||
useable=useable.PotionOfHealing(consumable=True, current_stack=1, maximum_stack=255),
|
useable=useable.PotionOfHealing(consumable=True, current_stack=1, maximum_stack=255),
|
||||||
@ -52,5 +52,18 @@ scroll_of_lightning = Entity(
|
|||||||
useable=useable.ScrollOfLightning(consumable=True, current_stack=1, maximum_stack=255, minimum_range=0, maximum_range=6),
|
useable=useable.ScrollOfLightning(consumable=True, current_stack=1, maximum_stack=255, minimum_range=0, maximum_range=6),
|
||||||
)
|
)
|
||||||
|
|
||||||
#TODO: scroll of confusion, using "confused AI"
|
scroll_of_confusion = Entity(
|
||||||
#TODO: scroll of fireball, dealing AOE damage
|
char = "!",
|
||||||
|
color = colors.navajo_white,
|
||||||
|
name = "Scroll of Confusion",
|
||||||
|
walkable = True,
|
||||||
|
useable=useable.ScrollOfConfusion(consumable=True, current_stack=1, maximum_stack=255, minimum_range=1, maximum_range=6),
|
||||||
|
)
|
||||||
|
|
||||||
|
scroll_of_fireball = Entity(
|
||||||
|
char = "!",
|
||||||
|
color = colors.orange_red,
|
||||||
|
name = "Scroll of Fireball",
|
||||||
|
walkable = True,
|
||||||
|
useable=useable.ScrollOfFireball(consumable=True, current_stack=1, maximum_stack=255, minimum_range=1, maximum_range=6),
|
||||||
|
)
|
@ -128,16 +128,25 @@ class EventHandler(tcod.event.EventDispatch[BaseAction]):
|
|||||||
|
|
||||||
class GameoverViewer(EventHandler):
|
class GameoverViewer(EventHandler):
|
||||||
"""Game over, man, GAME OVER!"""
|
"""Game over, man, GAME OVER!"""
|
||||||
|
def __init__(self,engine: Engine, parent_handler: EventHandler):
|
||||||
|
super().__init__(engine, parent_handler)
|
||||||
|
#Hacky fix, re-parent until you find the gameplay viewer
|
||||||
|
while self.parent_handler and self.parent_handler is not GameplayViewer:
|
||||||
|
self.parent_handler = self.parent_handler.parent_handler
|
||||||
|
|
||||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
||||||
key = event.sym #SDL stuff, neat.
|
key = event.sym #SDL stuff, neat.
|
||||||
|
|
||||||
#player input
|
#special keys
|
||||||
if key == tcod.event.KeySym.ESCAPE:
|
if key == tcod.event.KeySym.ESCAPE:
|
||||||
return QuitAction()
|
return QuitAction()
|
||||||
|
|
||||||
|
#menu keys
|
||||||
if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde
|
if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde
|
||||||
self.engine.event_handler = LogHistoryViewer(self.engine, self)
|
self.engine.event_handler = LogHistoryViewer(self.engine, self)
|
||||||
|
|
||||||
|
#TODO: read-only inventory viewer
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -175,7 +184,7 @@ class GameplayViewer(EventHandler):
|
|||||||
self.engine,
|
self.engine,
|
||||||
self,
|
self,
|
||||||
title = "Debug Selector",
|
title = "Debug Selector",
|
||||||
options = ["Tile Selector"],
|
options = ["Zero", "One", "Two", "Three", "Four", "Five", "Six"],
|
||||||
callback = lambda x: self.dbg_callback(x),
|
callback = lambda x: self.dbg_callback(x),
|
||||||
margin_x = 20,
|
margin_x = 20,
|
||||||
margin_y = 12,
|
margin_y = 12,
|
||||||
@ -186,16 +195,16 @@ class GameplayViewer(EventHandler):
|
|||||||
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]:
|
def dbg_callback(self, selected: int) -> Optional[BaseAction]:
|
||||||
if selected == 0:
|
player: Entity = self.engine.player
|
||||||
player: Entity = self.engine.player
|
|
||||||
|
|
||||||
self.engine.event_handler = TileSelector(
|
self.engine.event_handler = TileSelector(
|
||||||
self.engine,
|
self.engine,
|
||||||
self,
|
self,
|
||||||
floor_map = self.engine.floor_map,
|
floor_map = self.engine.floor_map,
|
||||||
initial_pointer = (player.x, player.y),
|
initial_cursor = (player.x, player.y),
|
||||||
callback=lambda: None
|
callback=lambda x, y: print(f"Fireball at ({x},{y})"),
|
||||||
)
|
cursor_radius=selected
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -371,16 +380,17 @@ class InventoryViewer(EventHandler):
|
|||||||
"""Use the item at the cursor's position."""
|
"""Use the item at the cursor's position."""
|
||||||
if self.length > 0:
|
if self.length > 0:
|
||||||
item: Entity = self.entity.inventory.access(self.cursor)
|
item: Entity = self.entity.inventory.access(self.cursor)
|
||||||
usable: BaseUseable = item.useable
|
useable: BaseUseable = item.useable
|
||||||
|
|
||||||
#for ranged items, delegate to the tile selector
|
#for ranged items, delegate to the tile selector
|
||||||
if usable.maximum_range > 0:
|
if useable.maximum_range > 0:
|
||||||
self.engine.event_handler = TileSelector(
|
self.engine.event_handler = TileSelector(
|
||||||
self.engine,
|
self.engine,
|
||||||
parent_handler=self.engine.event_handler,
|
parent_handler=self.engine.event_handler,
|
||||||
floor_map = self.engine.floor_map,
|
floor_map = self.engine.floor_map,
|
||||||
initial_pointer = (self.entity.x, self.entity.y),
|
initial_cursor = (self.entity.x, self.entity.y),
|
||||||
callback=lambda x, y: self.use_at_range(x, y)
|
callback=lambda x, y: self.use_at_range(x, y), #TODO: Radius is not rerolled on cancel
|
||||||
|
cursor_radius=0 if not hasattr(useable, 'radius') else useable.radius,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
#non-ranged items target the entity
|
#non-ranged items target the entity
|
||||||
@ -390,7 +400,6 @@ class InventoryViewer(EventHandler):
|
|||||||
#TODO: For now, just target living entities
|
#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))
|
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?
|
#TODO: close the inventory if you've used a consumable?
|
||||||
if len(targets) > 0:
|
if len(targets) > 0:
|
||||||
return UsageAction(self.entity, self.cursor, targets.pop(), lambda x: self.adjust_length(x))
|
return UsageAction(self.entity, self.cursor, targets.pop(), lambda x: self.adjust_length(x))
|
||||||
@ -497,6 +506,7 @@ class TileSelector(EventHandler):
|
|||||||
floor_map: FloorMap
|
floor_map: FloorMap
|
||||||
cursor_x: int
|
cursor_x: int
|
||||||
cursor_y: int
|
cursor_y: int
|
||||||
|
cursor_radius: int
|
||||||
callback: function
|
callback: function
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -505,12 +515,14 @@ class TileSelector(EventHandler):
|
|||||||
parent_handler,
|
parent_handler,
|
||||||
*,
|
*,
|
||||||
floor_map: FloorMap,
|
floor_map: FloorMap,
|
||||||
initial_pointer: Tuple[int, int],
|
initial_cursor: Tuple[int, int],
|
||||||
callback: function,
|
callback: function,
|
||||||
|
cursor_radius: int = 0,
|
||||||
):
|
):
|
||||||
super().__init__(engine, parent_handler)
|
super().__init__(engine, parent_handler)
|
||||||
self.floor_map = floor_map
|
self.floor_map = floor_map
|
||||||
self.cursor_x, self.cursor_y = initial_pointer
|
self.cursor_x, self.cursor_y = initial_cursor
|
||||||
|
self.cursor_radius = cursor_radius
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
|
||||||
def render(self, console: tcod.console.Console) -> None:
|
def render(self, console: tcod.console.Console) -> None:
|
||||||
@ -521,9 +533,19 @@ class TileSelector(EventHandler):
|
|||||||
if parent:
|
if parent:
|
||||||
parent.render(console)
|
parent.render(console)
|
||||||
|
|
||||||
#highlight via inverting colors
|
radius: int = max(self.cursor_radius, 0)
|
||||||
console.rgb["fg"][self.cursor_x, self.cursor_y] ^= 0xff
|
|
||||||
console.rgb["bg"][self.cursor_x, self.cursor_y] ^= 0xff
|
#radius is for display only - the item's actual effect is cacl'd elsewhere
|
||||||
|
for i in range(self.cursor_x - radius, self.cursor_x + radius + 1):
|
||||||
|
for j in range(self.cursor_y - radius, self.cursor_y + radius + 1):
|
||||||
|
#highlight via inverting colors
|
||||||
|
console.rgb["fg"][i, j] ^= 0xFF
|
||||||
|
console.rgb["bg"][i, j] ^= 0xFF
|
||||||
|
|
||||||
|
if radius > 0:
|
||||||
|
#re-apply an alteration, to show the center
|
||||||
|
console.rgb["fg"][self.cursor_x, self.cursor_y] ^= 0x4F
|
||||||
|
console.rgb["bg"][self.cursor_x, self.cursor_y] ^= 0x4F
|
||||||
|
|
||||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
||||||
key = event.sym #SDL stuff, neat.
|
key = event.sym #SDL stuff, neat.
|
||||||
|
@ -95,8 +95,13 @@ 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 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):
|
if not any(entity.x == x and entity.y == y for entity in floor_map.entities):
|
||||||
if random.random() < 0.8:
|
item_result = random.random()
|
||||||
|
if item_result < 0.2:
|
||||||
|
entity_types.scroll_of_fireball.spawn(x, y, floor_map)
|
||||||
|
elif item_result < 0.4:
|
||||||
entity_types.scroll_of_lightning.spawn(x, y, floor_map)
|
entity_types.scroll_of_lightning.spawn(x, y, floor_map)
|
||||||
|
elif item_result < 0.6:
|
||||||
|
entity_types.scroll_of_confusion.spawn(x, y, floor_map)
|
||||||
else:
|
else:
|
||||||
entity_types.potion_of_healing.spawn(x, y, floor_map)
|
entity_types.potion_of_healing.spawn(x, y, floor_map)
|
||||||
floor_map.procgen_cache["item_count"] += 1
|
floor_map.procgen_cache["item_count"] += 1
|
||||||
|
@ -47,7 +47,7 @@ class Stats:
|
|||||||
engine.message_log.add_message(f"The {self.entity.name} died", colors.green)
|
engine.message_log.add_message(f"The {self.entity.name} died", colors.green)
|
||||||
|
|
||||||
#transform into a dead body
|
#transform into a dead body
|
||||||
#TODO: dummied in a "usable" to let dead objects be treated like items
|
#TODO: dummied in a "useable" 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
|
||||||
|
@ -4,7 +4,7 @@ from typing import Optional, TYPE_CHECKING
|
|||||||
from utils import roll_dice
|
from utils import roll_dice
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from stats import Stats
|
from entity import Entity
|
||||||
|
|
||||||
class BaseUseable:
|
class BaseUseable:
|
||||||
"""
|
"""
|
||||||
@ -12,6 +12,7 @@ class BaseUseable:
|
|||||||
|
|
||||||
Please note that distances are calculated with the Pythagorean theorem, so a maximum range of `1` won't work in diagonally adjacent tiles.
|
Please note that distances are calculated with the Pythagorean theorem, so a maximum range of `1` won't work in diagonally adjacent tiles.
|
||||||
"""
|
"""
|
||||||
|
entity: Entity = None
|
||||||
consumable: bool
|
consumable: bool
|
||||||
current_stack: int
|
current_stack: int
|
||||||
maximum_stack: int
|
maximum_stack: int
|
||||||
@ -31,28 +32,26 @@ class BaseUseable:
|
|||||||
self.minimum_range = minimum_range
|
self.minimum_range = minimum_range
|
||||||
self.maximum_range = maximum_range
|
self.maximum_range = maximum_range
|
||||||
|
|
||||||
def apply(self, stats: Stats) -> bool:
|
def apply(self, target: Entity) -> bool:
|
||||||
"""
|
"""
|
||||||
Use this item's effects.
|
Use this item's effects to the `target`.
|
||||||
|
|
||||||
Returns `True` if the item's state changed.
|
Returns `True` if this item's state changed.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
def get_used_msg(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
May return a string to display to the user.
|
May return a string to display to the player, otherwise returns `None`.
|
||||||
|
|
||||||
`appearance` is what the item looks like, and can be substituted into the result.
|
|
||||||
"""
|
"""
|
||||||
return None #default
|
return None #default
|
||||||
|
|
||||||
#utils
|
#utils
|
||||||
def reduce_stack(self, amount: int = 1) -> bool:
|
def reduce_stack(self, amount: int = 1) -> bool:
|
||||||
"""
|
"""
|
||||||
Reduce the size of a stack by an amount.
|
Reduce the size of this item stack by a given `amount`.
|
||||||
|
|
||||||
Returns `True` if this item should be deleted.
|
Returns `True` if this entity should be deleted.
|
||||||
"""
|
"""
|
||||||
if self.maximum_stack > 0:
|
if self.maximum_stack > 0:
|
||||||
self.current_stack -= amount
|
self.current_stack -= amount
|
||||||
@ -79,11 +78,11 @@ class Unuseable(BaseUseable):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__() #enforce defaults
|
super().__init__() #enforce defaults
|
||||||
|
|
||||||
def apply(self, stats: Stats) -> bool:
|
def apply(self, target: Entity) -> bool:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
def get_used_msg(self) -> Optional[str]:
|
||||||
return f"This {appearance} is utterly useless."
|
return f"This {self.entity.name} is utterly useless."
|
||||||
|
|
||||||
|
|
||||||
class PotionOfHealing(BaseUseable):
|
class PotionOfHealing(BaseUseable):
|
||||||
@ -95,14 +94,14 @@ class PotionOfHealing(BaseUseable):
|
|||||||
"""Restores 4d4 health when applied."""
|
"""Restores 4d4 health when applied."""
|
||||||
__last_roll: int = -1
|
__last_roll: int = -1
|
||||||
|
|
||||||
def apply(self, stats: Stats) -> bool:
|
def apply(self, target: Entity) -> bool:
|
||||||
self.__last_roll = roll_dice(4, 4)
|
self.__last_roll = roll_dice(4, 4)
|
||||||
stats.current_hp += self.__last_roll
|
target.stats.current_hp += self.__last_roll
|
||||||
return self.reduce_stack()
|
return self.reduce_stack()
|
||||||
|
|
||||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
def get_used_msg(self) -> Optional[str]:
|
||||||
if self.__last_roll >= 0:
|
if self.__last_roll >= 0:
|
||||||
return f"The {appearance} restored {self.__last_roll} health."
|
return f"The {self.entity.name} restored {self.__last_roll} health."
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -110,15 +109,74 @@ class PotionOfHealing(BaseUseable):
|
|||||||
class ScrollOfLightning(BaseUseable):
|
class ScrollOfLightning(BaseUseable):
|
||||||
"""Deals 2d4 damage when applied."""
|
"""Deals 2d4 damage when applied."""
|
||||||
__last_roll: int = -1
|
__last_roll: int = -1
|
||||||
|
__last_target_name: str = ""
|
||||||
|
|
||||||
def apply(self, stats: Stats) -> bool:
|
def apply(self, target: Entity) -> bool:
|
||||||
self.__last_roll = roll_dice(2, 4)
|
self.__last_roll = roll_dice(2, 4)
|
||||||
stats.current_hp -= self.__last_roll
|
self.__last_target_name = target.name
|
||||||
|
target.stats.current_hp -= self.__last_roll
|
||||||
return self.reduce_stack()
|
return self.reduce_stack()
|
||||||
|
|
||||||
def get_used_msg(self, appearance: str) -> Optional[str]:
|
def get_used_msg(self) -> Optional[str]:
|
||||||
if self.__last_roll >= 0:
|
if self.__last_roll >= 0:
|
||||||
return f"The {appearance} dealt {self.__last_roll} damage."
|
return f"The {self.entity.name} dealt {self.__last_roll} damage to the {self.__last_target_name}!"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollOfConfusion(BaseUseable):
|
||||||
|
"""Causes 3d6 turns of confusion when applied."""
|
||||||
|
__last_roll: int = -1
|
||||||
|
__last_target_name: str = ""
|
||||||
|
|
||||||
|
def apply(self, target: Entity) -> bool:
|
||||||
|
from ai import ConfusedAI
|
||||||
|
|
||||||
|
self.__last_roll = roll_dice(3, 6)
|
||||||
|
self.__last_target_name = target.name
|
||||||
|
target.ai = ConfusedAI(target, self.__last_roll)
|
||||||
|
return self.reduce_stack()
|
||||||
|
|
||||||
|
def get_used_msg(self) -> Optional[str]:
|
||||||
|
if self.__last_roll >= 0:
|
||||||
|
return f"The {self.__last_target_name} looks confused?"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ScrollOfFireball(BaseUseable):
|
||||||
|
"""Deals 3d6 damage to all creatures within 1d6 radius of the target when applied.""" #TODO: Take a list of targets
|
||||||
|
__last_damage_roll: int = -1
|
||||||
|
__last_radius_roll: int = -1
|
||||||
|
__last_target_name: str = ""
|
||||||
|
__last_entity_hurt: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def radius(self) -> int: #TODO: awkwardly hacked in, will fix later
|
||||||
|
if self.__last_radius_roll < 0:
|
||||||
|
self.__last_radius_roll = roll_dice(1, 6)
|
||||||
|
return self.__last_radius_roll
|
||||||
|
|
||||||
|
def reroll_radius(self) -> int:
|
||||||
|
self.__last_radius_roll = -1
|
||||||
|
return self.radius
|
||||||
|
|
||||||
|
def apply(self, target: Entity) -> bool:
|
||||||
|
self.__last_damage_roll = roll_dice(3, 6)
|
||||||
|
self.__last_target_name = target.name
|
||||||
|
|
||||||
|
#find and apply to all living entities within range
|
||||||
|
for entity in target.floor_map.entities:
|
||||||
|
if entity.is_alive():
|
||||||
|
if target.x - self.radius <= entity.x <= target.x + self.radius:
|
||||||
|
if target.y - self.radius <= entity.y <= target.y + self.radius:
|
||||||
|
entity.stats.current_hp -= self.__last_damage_roll
|
||||||
|
|
||||||
|
return self.reduce_stack()
|
||||||
|
|
||||||
|
def get_used_msg(self) -> Optional[str]:
|
||||||
|
if self.__last_damage_roll >= 0:
|
||||||
|
return f"The {self.entity.name} dealt {self.__last_damage_roll} damage to the {self.__last_target_name} and the surrounding creatures!"
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user