Refactor to my personal tastes

This commit is contained in:
Kayne Ruse 2025-03-28 14:07:12 +11:00
parent d264b08ce8
commit 59fa1ba929
15 changed files with 263 additions and 207 deletions

View File

@ -1,18 +1,28 @@
from typing import Any
from __future__ import annotations
from typing import TYPE_CHECKING
import colors
from floor_map import FloorMap
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
class BaseAction:
entity: Any
entity: Entity #the entity to which this action applies
def __init__(self, entity):
self.entity = entity
def apply(self) -> bool:
"""return True if the game state should be progressed"""
raise NotImplementedError()
class QuitAction(BaseAction):
def __init__(self): #override the base __init__
def __init__(self):
"""override the base __init__, as no entity is needed"""
pass
def apply(self) -> bool:
@ -24,66 +34,80 @@ class WaitAction(BaseAction):
return True
class DirectionAction(BaseAction):
class MovementAction(BaseAction):
"""Move an Entity within the map"""
def __init__(self, entity, xdir: int, ydir: int):
super().__init__(entity)
self.xdir = xdir
self.ydir = ydir
def apply(self) -> bool:
raise NotImplementedError()
class MovementAction(DirectionAction):
def apply(self) -> bool:
dest_x = self.entity.x + self.xdir
dest_y = self.entity.y + self.ydir
floor_map: FloorMap = self.entity.floor_map
#bounds and collision checks
if not self.entity.floor_map.in_bounds(dest_x, dest_y):
if not floor_map.in_bounds(dest_x, dest_y):
return False
if not self.entity.floor_map.tiles["walkable"][dest_x, dest_y]:
if not floor_map.tiles["walkable"][dest_x, dest_y]:
return False
if self.entity.floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True) is not None:
if floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True) is not None:
return False
self.entity.set_pos(dest_x, dest_y)
return True
class MeleeAction(DirectionAction):
class MeleeAction(BaseAction):
"""Melee attack from the Entity towards a target"""
def __init__(self, entity, xdir: int, ydir: int):
super().__init__(entity)
self.xdir = xdir
self.ydir = ydir
def apply(self) -> bool:
dest_x = self.entity.x + self.xdir
dest_y = self.entity.y + self.ydir
target = self.entity.floor_map.get_actor_at(dest_x, dest_y)
target = self.entity.floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True)
if not target:
if not target or not target.stats:
return False
#apply damage
damage = self.entity.fighter.attack - target.fighter.defense
target.fighter.current_hp -= damage
#TODO: better combat system
#calculate output
engine = self.entity.floor_map.engine
#calculate damage
damage = self.entity.stats.attack - target.stats.defense
#calculate message output
engine: Engine = self.entity.floor_map.engine
msg_text = f"{self.entity.name} attacked {target.name}"
msg_color = colors.white
if self.entity is engine.player:
msg_color = colors.player_atk
msg_color = colors.white
else:
msg_color = colors.enemy_atk
msg_color = colors.white
if damage > 0:
msg_text += f" for {damage} damage"
else:
msg_text += f" but was ineffective"
engine.message_log.add_message(text = msg_text, fg=msg_color)
engine.message_log.add_message(text = msg_text, color=msg_color)
#actually applying the change here, so the player's death event is at the bottom of the message log
target.stats.current_hp -= damage
return True
class BumpAction(DirectionAction): #bad name, deal with it
class BumpAction(BaseAction):
"""Move an Entity within the map, or attack a target if one is found"""
def __init__(self, entity, xdir: int, ydir: int):
super().__init__(entity)
self.xdir = xdir
self.ydir = ydir
def apply(self) -> bool:
dest_x = self.entity.x + self.xdir
dest_y = self.entity.y + self.ydir

View File

@ -1,30 +1,36 @@
from __future__ import annotations
from typing import Any, List, Tuple
from typing import List, Tuple, TYPE_CHECKING
import numpy as np
import tcod
from components.base_component import BaseComponent
from actions import BaseAction, MeleeAction, MovementAction, WaitAction
if TYPE_CHECKING:
from entity import Entity
class BaseAI(BaseAction, BaseComponent):
entity: Any
class BaseAI:
"""Base type for monster AI, with various utilities"""
entity: Entity
def apply(self) -> None:
def __init__(self, entity):
self.entity = entity
self.path: List[Tuple[int, int]] = []
def process(self) -> BaseAction:
"""Decides what action to take"""
raise NotImplementedError()
def get_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
cost = np.array(self.entity.floor_map.tiles["walkable"], dtype=np.int8)
#higher numbers deter path-finding this way
for entity in self.entity.floor_map.entities:
if not entity.walkable and cost[entity.x, entity.y]:
cost[entity.x, entity.y] += 10
cost[entity.x, entity.y] += 100
graph = tcod.path.SimpleGraph(cost=cost, cardinal=2, diagonal=3)
graph = tcod.path.SimpleGraph(cost=cost, cardinal=10, diagonal=14)
pathfinder = tcod.path.Pathfinder(graph)
pathfinder.add_root((self.entity.x, self.entity.y)) #start pos
@ -35,30 +41,34 @@ class BaseAI(BaseAction, BaseComponent):
#return the path, after mapping it to a list of tuples
return [(index[0], index[1]) for index in path]
class AttackOnSight(BaseAI):
def __init__(self, entity: Actor):
super().__init__(entity)
self.path: List[Tuple[int, int]] = []
def apply(self) -> None:
class AggressiveWhenSeen(BaseAI):
"""
If the player can seem me, try to approach and attack.
Otherwise, idle.
"""
def process(self) -> BaseAction:
target = self.entity.floor_map.player
xdir = target.x - self.entity.x
ydir = target.y - self.entity.y
distance = max(abs(xdir), abs(ydir))
#if the player can see me, and I'm close enough, attack
#if the player can see me
if self.entity.floor_map.visible[self.entity.x, self.entity.y]:
#if I'm close enough to attack
if distance <= 1:
return MeleeAction(self.entity, xdir, ydir).apply()
return MeleeAction(self.entity, xdir, ydir)
self.path = self.get_path_to(target.x, target.y)
self.path = self.generate_path_to(target.x, target.y)
#if I have a path to follow
if self.path:
dest_x, dest_y = self.path.pop(0)
return MovementAction(
entity = self.entity,
xdir = dest_x - self.entity.x,
ydir = dest_y - self.entity.y,
).apply()
)
return WaitAction(self.entity).apply()
#idle
return WaitAction(self.entity)

View File

@ -1,15 +1,21 @@
#copy/pasted, because reasons
#Standard colors
white = (0xFF, 0xFF, 0xFF)
black = (0x0, 0x0, 0x0)
player_atk = (0xE0, 0xE0, 0xE0)
enemy_atk = (0xFF, 0xC0, 0xC0)
red = (0xFF, 0, 0)
green = (0, 0xFF, 0)
blue = (0, 0, 0xFF)
player_die = (0xFF, 0x30, 0x30)
enemy_die = (0xFF, 0xA0, 0x30)
yellow = (0xFF, 0xFF, 0)
magenta = (0xFF, 0, 0xFF)
cyan = (0, 0xFF, 0xFF)
welcome_text = (0x20, 0xA0, 0xFF)
#gameboy DMG-01, according to Wikipedia's CSS
gameboy_00 = (0x29, 0x41, 0x39)
gameboy_01 = (0x39, 0x59, 0x4a)
gameboy_02 = (0x5a, 0x79, 0x42)
gameboy_03 = (0x7b, 0x82, 0x10)
bar_text = white
bar_filled = (0x0, 0x60, 0x0)
bar_empty = (0x40, 0x10, 0x10)
#terminal-like
terminal_light = (200, 200, 200)
terminal_dark = (100, 100, 100)

View File

@ -1,5 +0,0 @@
from typing import Any
class BaseComponent:
entity: Any

View File

@ -1,50 +1,69 @@
from __future__ import annotations
from typing import List
from tcod.context import Context
from tcod.console import Console
from tcod.map import compute_fov
from entity import Entity
from message_log import Message, MessageLog
from render_functions import render_hp_bar, render_names_at_location
from floor_map import FloorMap #TODO: replace with "DungeonMap" or similar
from render_functions import render_hp_bar, render_names_at
from floor_map import FloorMap #TODO: replace with "DungeonMap"
class Engine:
def __init__(self, floor_map: FloorMap, intro_msg: Message = None, ui_width: int = None, ui_height: int = None):
player: Entity
floor_map: FloorMap
def __init__(self, *, floor_map: FloorMap, initial_log: List[Message] = None, ui_width: int = None, ui_height: int = None):
#events
from event_handler import InGameHandler
self.event_handler = InGameHandler(self)
self.mouse_location = (0, 0)
from event_handlers import GameplayHandler
self.event_handler = GameplayHandler(self)
self.mouse_position = (0, 0)
#map
self.floor_map = floor_map
self.floor_map.engine = self #references everywhere!
self.floor_map.engine = self #entities in maps can also reference the engine
#messages
self.message_log = MessageLog()
if intro_msg:
self.message_log.push_message(intro_msg)
if initial_log:
self.message_log.push_messages(initial_log)
#grab the player object
#grab the player object (generated by the procgen, usually)
self.player = self.floor_map.player
#default values
self.ui_width = floor_map.width if ui_width is None else ui_width
self.ui_height = 0 if ui_height is None else ui_height
#kick off the render
#kick off the fov
self.update_fov()
def run_loop(self, context: Context, console: Console) -> None:
while True:
self.update_fov()
if self.event_handler.handle_events(context):
self.handle_entities()
self.handle_entities() #TODO: what 'game state'?
self.handle_rendering(context, console)
def handle_entities(self) -> None:
self.update_fov() #knowing the FOV lets entities mess with it
def handle_entities(self) -> bool:
"""
Processes monster AI and other things.
Returns `True` if the game state should be progressed.
"""
result = False
#all *actors* in the level
for actor in set(self.floor_map.actors) - {self.player}:
if actor.ai:
actor.ai.apply()
#make the entities think and act
for entity in set(self.floor_map.entities) - {self.player}:
if entity.ai:
action = entity.ai.process()
result |= action.apply()
return result
def handle_rendering(self, context: Context, console: Console) -> None:
#map and all entities within
@ -55,11 +74,11 @@ class Engine:
console = console,
x = 0,
y = self.floor_map.height,
current_value = self.player.fighter.current_hp,
max_value = self.player.fighter.maximum_hp,
current_value = self.player.stats.current_hp,
max_value = self.player.stats.maximum_hp,
total_width = self.ui_width // 2,
)
render_names_at_location(
render_names_at(
console = console,
x = 1,
y = self.floor_map.height + 2,

View File

@ -3,8 +3,8 @@ from __future__ import annotations
import copy
from typing import Optional, Tuple, Type
from components.base_ai import BaseAI
from components.fighter import Fighter
from ai import BaseAI
from stats import Stats
class Entity:
def __init__(
@ -16,6 +16,10 @@ class Entity:
name: str = "<Unnamed>",
walkable: bool = True,
floor_map = None,
#monster-specific stuff
ai_class: Type[BaseAI] = None,
stats: Stats = None,
):
self.x = x
self.y = y
@ -25,6 +29,15 @@ class Entity:
self.walkable = walkable
self.floor_map = floor_map
#monster-specific stuff
if ai_class:
self.ai: Optional[BaseAI] = ai_class(self)
if stats:
self.stats = stats
self.stats.entity = self
#generic entity stuff
def spawn(self, x: int, y: int, floor_map):
clone = copy.deepcopy(self)
clone.x = x
@ -36,37 +49,7 @@ class Entity:
def set_pos(self, x: int, y: int) -> None:
self.x = x
self.y = y
#actors are entities that can act on their own
class Actor(Entity):
def __init__( #yep, this is ugly
self,
x: int = 0,
y: int = 0,
char: str = "?",
color: Tuple[int, int, int] = (255, 255, 255),
name: str = "<Unnamed>",
walkable: bool = True,
floor_map = None,
#actor-specific stuff
ai_class: Type[BaseAI] = None,
fighter: Fighter = None,
):
super().__init__(
x = x,
y = y,
char = char,
color = color,
name = name,
walkable = walkable,
floor_map = floor_map,
)
self.ai: Optional[BaseAI] = ai_class(self)
self.fighter = fighter
self.fighter.entity = self
#monster-specific stuff
def is_alive(self) -> bool:
return bool(self.ai)

View File

@ -1,31 +1,31 @@
from entity import Entity, Actor
from components.base_ai import BaseAI, AttackOnSight
from components.fighter import Fighter
from entity import Entity
from ai import BaseAI, AggressiveWhenSeen
from stats import Stats
player = Actor(
player = Entity(
char = "@",
color = (255, 255, 255),
name = "Player",
walkable = False,
ai_class = BaseAI,
fighter = Fighter(hp = 10, attack = 2, defense = 1),
ai_class = BaseAI, #TODO: remove this or dummy it out
stats = Stats(hp = 10, attack = 2, defense = 1),
)
#gobbos
gobbo = Actor(
gobbo = Entity(
char = "g",
color = (30, 168, 41),
name = "Gobbo",
walkable = False,
ai_class = AttackOnSight,
fighter = Fighter(hp = 5, attack = 1, defense = 0),
ai_class = AggressiveWhenSeen,
stats = Stats(hp = 5, attack = 1, defense = 0),
)
gobbo_red = Actor(
gobbo_red = Entity(
char = "g",
color = (168, 41, 30),
name = "Red Gobbo",
walkable = False,
ai_class = AttackOnSight,
fighter = Fighter(hp = 5, attack = 2, defense = 1),
ai_class = AggressiveWhenSeen,
stats = Stats(hp = 5, attack = 2, defense = 5),
)

View File

@ -1,9 +1,12 @@
from typing import Optional
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
import tcod
from actions import BaseAction, QuitAction, BumpAction, WaitAction
from engine import Engine
if TYPE_CHECKING:
from engine import Engine
#input options
MOVE_KEYS = {
@ -57,8 +60,10 @@ CURSOR_SCROLL_KEYS = {
tcod.event.KeySym.KP_8: -1,
}
#event handler is one part of the engine
#the event handlers are one part of the engine
class EventHandler(tcod.event.EventDispatch[BaseAction]):
engine: Engine
def __init__(self, engine: Engine):
super().__init__()
self.engine = engine
@ -71,21 +76,22 @@ class EventHandler(tcod.event.EventDispatch[BaseAction]):
return QuitAction()
def handle_events(self, context: tcod.context.Context) -> bool:
"""If any Action signals True, then the game state should be progressed after this"""
result = False
for event in tcod.event.wait():
context.convert_event(event)
context.convert_event(event) #adds mouse position info
action = self.dispatch(event)
if action is None:
continue
result |= action.apply() #entity references the engine
result |= action.apply()
return result
class InGameHandler(EventHandler):
class GameplayHandler(EventHandler):
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
key = event.sym #SDL stuff, neat.
@ -102,8 +108,8 @@ class InGameHandler(EventHandler):
if key in WAIT_KEYS:
return WaitAction(player)
if key == tcod.event.KeySym.v:
self.engine.event_handler = LogHistoryViewer(self.engine)
if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde
self.engine.event_handler = LogHistoryViewer(self.engine, self)
def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
if self.engine.floor_map.in_bounds(event.tile.x, event.tile.y):
@ -111,6 +117,7 @@ class InGameHandler(EventHandler):
class GameOverHandler(EventHandler):
"""Game over, man, GAME OVER!"""
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
key = event.sym #SDL stuff, neat.
@ -118,12 +125,18 @@ class GameOverHandler(EventHandler):
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 LogHistoryViewer(EventHandler):
def __init__(self, engine: Engine):
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
@ -132,7 +145,7 @@ class LogHistoryViewer(EventHandler):
log_console = tcod.console.Console(console.width - 6, console.height - 6)
#custom...
#rendering a nice log window
log_console.draw_frame(0, 0, log_console.width, log_console.height)
log_console.print_box(
0, 0, log_console.width, 1, "Message History", alignment=tcod.constants.CENTER
@ -161,4 +174,4 @@ class LogHistoryViewer(EventHandler):
self.cursor = self.log_length - 1
else:
#return to the game
self.engine.event_handler = InGameHandler(self.engine)
self.engine.event_handler = self.baseEventHandler

View File

@ -1,13 +1,19 @@
from __future__ import annotations
from typing import Iterable, Iterator, Optional
from typing import Iterable, Optional, Set, TYPE_CHECKING
import numpy as np
from tcod.console import Console
import tile_types
from entity import Entity, Actor
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
class FloorMap:
engine: Engine
player: Entity
def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
#terrain stuff
self.width, self.height = width, height
@ -17,25 +23,17 @@ class FloorMap:
self.explored = np.full((width, height), fill_value=False, order="F")
#contents stuff
self.entities = set(entities)
self.entities: Set[Entity] = set(entities)
self.procgen_cache = {} #reserved for the procgen algorithm, otherwise ignored
#set externally
self.engine = None
self.player = None
@property
def actors(self) -> Iterator[Actor]:
yield from (
entity
for entity in self.entities
if isinstance(entity, Actor) and entity.is_alive()
)
def in_bounds(self, x: int, y: int) -> bool:
return 0 <= x < self.width and 0 <= y < self.height
def get_entity_at(self, x: int, y: int, unwalkable_only: bool = False) -> Optional[Entity]:
def get_entity_at(self, x: int, y: int, *, unwalkable_only: bool = False) -> Optional[Entity]:
for entity in self.entities:
if entity.x == x and entity.y == y:
if unwalkable_only:
@ -46,13 +44,6 @@ class FloorMap:
return None
def get_actor_at(self, x: int, y: int) -> Optional[Actor]:
for actor in self.actors:
if actor.x == x and actor.y == y and actor.is_alive():
return actor
return None
def render(self, console: Console) -> None:
console.rgb[0:self.width, 0:self.height] = np.select(
condlist = [self.visible, self.explored],
@ -74,4 +65,4 @@ class FloorMap:
#print the player above everything else for clarity
if self.player:
console.print(self.player.x, self.player.y, self.player.char, fg=self.player.color) #TODO: I didn't realize the render order would be fixed in the tutorial
console.print(self.player.x, self.player.y, self.player.char, fg=self.player.color)

View File

@ -9,11 +9,13 @@ import colors
def main() -> None:
#screen dimensions depend partially on the tileset
tileset = tcod.tileset.load_tilesheet("assets/dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD)
#TODO: see if there's a nicer tilesheet
#how big is the map's dimensions
map_width = 80
map_height = 40
#how big is the UI panel
ui_height = 5
#tcod stuff
@ -30,8 +32,14 @@ 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),
intro_msg = Message("Welcome to the Cave of Gobbos!", colors.welcome_text),
ui_height = ui_height,
initial_log= [
Message(" Movement: Numpad", colors.terminal_light),
Message(" See Log: Backtick", colors.terminal_light),
Message(" Quit: Esc", colors.terminal_light),
Message("Welcome to the Cave of Gobbos!", colors.cyan),
]
)
#game loop that never returns

View File

@ -5,11 +5,11 @@ from tcod.console import Console
import colors
#util class
class Message:
def __init__(self, text: str, fg: Tuple[int, int, int] = colors.white, count: int = 1):
def __init__(self, text: str, color: Tuple[int, int, int] = colors.white, count: int = 1):
self.raw_text = text
self.fg = fg
self.color = color
self.count = count
@property
@ -23,14 +23,14 @@ class MessageLog:
def __init__(self):
self.messages: List[Message] = []
def add_message(self, text: str, fg: Tuple[int, int, int] = colors.white, *, stack: bool = True) -> None:
def add_message(self, text: str, color: Tuple[int, int, int] = colors.white, *, stack: bool = True) -> None:
if stack and self.messages and text == self.messages[-1].raw_text:
self.messages[-1].count += 1
else:
self.messages.append(Message(text, fg))
self.messages.append(Message(text, color))
def push_message(self, msg: Message) -> None:
self.messages.append(msg)
def push_messages(self, msg_list: List[Message]) -> None:
self.messages.extend(msg_list)
def render(self, console: Console, x: int, y: int, width: int, height: int) -> None:
self.render_messages(console, x, y, width, height, self.messages)
@ -43,7 +43,7 @@ class MessageLog:
for message in reversed(messages):
for line in reversed(wrapper.wrap(message.full_text)): #oh, neat
console.print(x=x,y=y + y_offset,string=line,fg=message.fg)
console.print(x=x,y=y + y_offset,string=line,fg=message.color)
y_offset -= 1
if y_offset < 0:
return

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import random
from typing import Iterator, List, Tuple
import random
import tcod
@ -76,17 +75,17 @@ def spawn_monsters(floor_map: FloorMap, room: RectangularRoom, room_monsters_max
#generators
def generate_floor_map(
map_width: int,
map_height: int,
room_width_max: int,
room_height_max: int,
room_width_min: int = 6,
room_height_min: int = 6,
room_count_max: int = 20,
room_monsters_max: int = 2
) -> FloorMap:
map_width: int,
map_height: int,
room_width_max: int,
room_height_max: int,
room_width_min: int = 6,
room_height_min: int = 6,
room_count_max: int = 20,
room_monsters_max: int = 2
) -> FloorMap:
#simplistic floor generator
floor_map = FloorMap(map_width, map_height)
floor_map: FloorMap = FloorMap(map_width, map_height)
rooms: List[RectangularRoom] = []
@ -107,8 +106,6 @@ def generate_floor_map(
if len(rooms) == 0:
x, y = new_room.center
floor_map.player = entity_types.player.spawn(x, y, floor_map)
floor_map.entities.add(floor_map.player) #get it working first
else:
for x, y in make_corridor(rooms[-1].center, new_room.center):
floor_map.tiles[x, y] = tile_types.floor

View File

@ -1,11 +1,13 @@
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING
from tcod.console import Console
import colors
from floor_map import FloorMap
if TYPE_CHECKING:
from engine import Engine
from floor_map import FloorMap
#utils
def get_names_at(x: int, y: int, floor_map: FloorMap) -> str:
@ -22,15 +24,15 @@ def get_names_at(x: int, y: int, floor_map: FloorMap) -> str:
def render_hp_bar(console: Console, x: int, y: int, current_value: int, max_value: int, total_width: int) -> None:
bar_width = int(float(current_value) / max_value * total_width)
console.draw_rect(x=x, y=y, width=total_width, height=1, ch=1, bg=colors.bar_empty)
console.draw_rect(x=x, y=y, width=total_width, height=1, ch=1, bg=colors.terminal_dark)
if bar_width > 0:
console.draw_rect(x=x, y=y, width=bar_width, height=1, ch=1, bg=colors.bar_filled)
console.draw_rect(x=x, y=y, width=bar_width, height=1, ch=1, bg=colors.green)
console.print(x=x + 1, y=y, string=f"HP: {current_value}/{max_value}", fg=colors.bar_text)
console.print(x=x + 1, y=y, string=f"HP: {current_value}/{max_value}", fg=colors.white)
def render_names_at_location(console: Console, x: int, y: int, engine: Any) -> None:
mouse_x, mouse_y = engine.mouse_location
def render_names_at(console: Console, x: int, y: int, engine: Engine) -> None:
mouse_x, mouse_y = engine.mouse_position
names: str = get_names_at(mouse_x, mouse_y, engine.floor_map)

View File

@ -1,12 +1,19 @@
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING
import colors
from components.base_component import BaseComponent
class Fighter(BaseComponent):
entity: Any
from event_handlers import GameOverHandler
if TYPE_CHECKING:
from engine import Engine
from entity import Entity
class Stats:
"""Handles stats for an Entity"""
entity: Entity
#TODO: better combat system
def __init__(self, hp: int, attack: int, defense: int):
self._maximum_hp = hp
@ -24,24 +31,25 @@ class Fighter(BaseComponent):
@current_hp.setter
def current_hp(self, value: int) -> None:
"""Clamps to (0,maximum_hp), and calls `die_and_despawn()` if needed"""
self._current_hp = max(0, min(value, self._maximum_hp))
if self.current_hp <= 0:
self.die_and_despawn()
def die_and_despawn(self) -> None:
engine = self.entity.floor_map.engine
engine: Engine = self.entity.floor_map.engine
if self.entity is engine.player and self.entity.ai:
from event_handler import GameOverEventHandler
self.entity.floor_map.engine.event_handler = GameOverEventHandler(self.entity.floor_map.engine)
engine.message_log.add_message("You died.", colors.player_die)
if self.entity is engine.player and self.entity.ai: #handle game-over states
engine.event_handler = GameOverHandler(engine)
engine.message_log.add_message("You died.", colors.red)
else:
engine.message_log.add_message(f"The {self.entity.name} died", colors.enemy_die)
engine.message_log.add_message(f"The {self.entity.name} died", colors.yellow)
#transform into a dead body
self.entity.char = "%"
self.entity.color = (191, 0, 0)
self.entity.walkable = True
self.entity.ai = None
self.entity.ai = None #TODO: Could decay over time
self.entity.name = f"Dead {self.entity.name}"

View File

@ -1,8 +1,8 @@
from typing import Tuple
import numpy as np
#datatypes
import colors
#nympy datatypes
graphics_dt = np.dtype(
[
("ch", np.int32),
@ -20,22 +20,22 @@ tile_dt = np.dtype(
]
)
def new_tile(*, walkable: np.bool, transparent: np.bool, light: graphics_dt, dark: graphics_dt):
def new_tile(*, walkable: np.bool, transparent: np.bool, light: graphics_dt, dark: graphics_dt): # type: ignore
return np.array((walkable, transparent, light, dark), dtype = tile_dt)
#list of tile types
SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=graphics_dt)
SHROUD = np.array((ord(" "), colors.white, colors.black), dtype=graphics_dt)
wall = new_tile(
walkable=False,
transparent=False,
light=(ord('#'), (200, 200, 200), (0, 0, 0)),
dark =(ord('#'), (100, 100, 100), (0, 0, 0)),
light=(ord('#'), colors.terminal_light, colors.black),
dark =(ord('#'), colors.terminal_dark, colors.black),
)
floor = new_tile(
walkable=True,
transparent=True,
light=(ord('.'), (200, 200, 200), (0, 0, 0)),
dark =(ord('.'), (100, 100, 100), (0, 0, 0)),
light=(ord('.'), colors.terminal_light, colors.black),
dark =(ord('.'), colors.terminal_dark, colors.black),
)