"""
Main Window Manager.
"""
from typing import Optional, Tuple
import arcade
import json
import pyglet.gl as gl
from constants import *
from entities.entity import Entity
from status_bar import draw_status_bar
from game_engine import GameEngine
from util import pixel_to_char
from util import char_to_pixel
from themes.current_theme import colors
[docs]class MyGame(arcade.Window):
"""
Main application class.
Manage the GUI
"""
[docs] def __init__(self, width: int, height: int, title: str):
"""
:param width:
:param height:
:param title:
"""
super().__init__(width, height, title, antialiasing=False)
# Main game engine, where the game is managed
self.game_engine = GameEngine()
# Track the current state of what key is pressed
self.left_pressed = False
self.right_pressed = False
self.up_pressed = False
self.down_pressed = False
self.up_left_pressed = False
self.up_right_pressed = False
self.down_left_pressed = False
self.down_right_pressed = False
# Used for auto-repeat of moves
self.time_since_last_move_check = 0
# Where is the mouse?
self.mouse_position: Optional[Tuple[float, float]] = None
self.mouse_over_text: Optional[str] = None
# These are sprites that appear as buttons on the character sheet.
self.character_sheet_buttons = arcade.SpriteList()
arcade.set_background_color(colors['background'])
[docs] def setup(self):
""" Set up the game here. Call this function to restart the game. """
self.game_engine.setup()
for button_name, y_value in zip(
["attack", "defense", "hp", "capacity"],
range(SCREEN_HEIGHT - 75, 490, -37)
):
sprite = arcade.Sprite("images/plus_button.png")
sprite.center_x = 200
sprite.center_y = y_value
sprite.name = button_name
self.character_sheet_buttons.append(sprite)
[docs] def draw_hp_and_status_bar(self):
text = f"HP: {self.game_engine.player.fighter.hp}/{self.game_engine.player.fighter.max_hp}"
arcade.draw_text(text, 0, 0, colors["status_panel_text"])
if self.game_engine.player.fighter.level <= len(EXPERIENCE_PER_LEVEL):
xp_to_next_level = EXPERIENCE_PER_LEVEL[self.game_engine.player.fighter.level-1]
text = f"XP: {self.game_engine.player.fighter.current_xp:,}/{xp_to_next_level:,}"
else:
text = f"XP: {self.game_engine.player.fighter.current_xp:,}"
arcade.draw_text(text, 100, 0, colors["status_panel_text"])
text = f"Level: {self.game_engine.player.fighter.level}"
arcade.draw_text(text, 200, 0, colors["status_panel_text"])
size = 65
margin = 2
draw_status_bar(
size / 2 + margin,
24,
size,
10,
self.game_engine.player.fighter.hp,
self.game_engine.player.fighter.max_hp,
)
[docs] def draw_inventory(self):
capacity = self.game_engine.player.inventory.capacity
selected_item = self.game_engine.selected_item
field_width = SCREEN_WIDTH / (capacity + 1)
for i in range(capacity):
y = 40
x = i * field_width
if i == selected_item:
arcade.draw_lrtb_rectangle_outline(
x - 1, x + field_width - 5, y + 20, y, arcade.color.BLACK, 2
)
if self.game_engine.player.inventory.items[i]:
item_name = self.game_engine.player.inventory.items[i].name
else:
item_name = ""
text = f"{i + 1}: {item_name}"
arcade.draw_text(text, x, y, colors["status_panel_text"])
[docs] def draw_mouse_over_text(self):
if self.mouse_over_text:
x, y = self.mouse_position
arcade.draw_xywh_rectangle_filled(x, y, 100, 16, arcade.color.BLACK)
arcade.draw_text(self.mouse_over_text, x, y, arcade.csscolor.WHITE)
[docs] def draw_in_normal_state(self):
self.draw_hp_and_status_bar()
self.draw_inventory()
self.handle_messages()
self.draw_messages()
self.draw_mouse_over_text()
[docs] def draw_in_select_location_state(self):
# If mouse hasn't been over the window yet, return None
if self.mouse_position is None:
return
mouse_x, mouse_y = self.mouse_position
grid_x, grid_y = pixel_to_char(mouse_x, mouse_y)
center_x, center_y = char_to_pixel(grid_x, grid_y)
arcade.draw_rectangle_outline(
center_x,
center_y,
SPRITE_WIDTH,
SPRITE_HEIGHT,
arcade.color.LIGHT_BLUE,
2,
)
[docs] def draw_character_screen(self):
arcade.draw_xywh_rectangle_filled(
0,
0,
SCREEN_WIDTH,
SCREEN_HEIGHT,
colors["status_panel_background"],
)
spacing = 1.8
y_value = SCREEN_HEIGHT - 50
x_value = 10
text_size = 24
text = "Character Screen"
arcade.draw_text(text, x_value, y_value, colors['status_panel_text'], text_size)
y_value -= text_size * spacing
text_size = 20
texts = [
f"Attack: {self.game_engine.player.fighter.power}",
f"Defense: {self.game_engine.player.fighter.defense}",
f"HP: {self.game_engine.player.fighter.hp} / {self.game_engine.player.fighter.max_hp}",
f"Max Inventory: {self.game_engine.player.inventory.capacity}",
f"Level: {self.game_engine.player.fighter.level}",
]
for text in texts:
arcade.draw_text(text, x_value, y_value, colors['status_panel_text'], text_size)
y_value -= text_size * spacing
if self.game_engine.player.fighter.ability_points > 0:
self.character_sheet_buttons.draw()
[docs] def handle_messages(self):
# Check message queue. Limit to 2 lines
while len(self.game_engine.messages) > 2:
self.game_engine.messages.pop(0)
[docs] def draw_messages(self):
y = 20
for message in self.game_engine.messages:
arcade.draw_text(message, 300, y, colors["status_panel_text"])
y -= 20
[docs] def draw_sprites_and_status_panel(self):
# Draw the sprites
self.game_engine.cur_level.dungeon_sprites.draw(filter=gl.GL_NEAREST)
self.game_engine.cur_level.entities.draw(filter=gl.GL_NEAREST)
self.game_engine.cur_level.creatures.draw(filter=gl.GL_NEAREST)
self.game_engine.characters.draw(filter=gl.GL_NEAREST)
# Draw the status panel
arcade.draw_xywh_rectangle_filled(
0,
0,
SCREEN_WIDTH,
STATUS_PANEL_HEIGHT,
colors["status_panel_background"],
)
[docs] def handle_character_screen_click(self, x: float, y: float):
if self.game_engine.player.fighter.ability_points > 0:
sprites_clicked = arcade.get_sprites_at_point((x, y), self.character_sheet_buttons)
button_names_to_effects = {
"attack": {"area": "fighter", "trait": "power", "change": 1},
"defense": {"area": "fighter", "trait": "defense", "change": 1},
"hp": {"area": "fighter", "trait": "max_hp", "change": 5},
"capacity": {"area": "inventory", "trait": "capacity", "change": 1},
}
for sprite in sprites_clicked:
if sprite.name in button_names_to_effects:
effect = button_names_to_effects[sprite.name]
area = getattr(self.game_engine.player, effect["area"])
original_value = getattr(area, effect["trait"])
setattr(area, effect["trait"], original_value + effect["change"])
self.game_engine.player.fighter.ability_points -= 1
[docs] def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
"""
Handle mouse-down events
:param x:
:param y:
:param button:
:param modifiers:
"""
# If we are currently in a 'select location' state, process
if self.game_engine.game_state == STATE.SELECT_LOCATION:
# Grab grid location
grid_x, grid_y = pixel_to_char(x, y)
# Notify game engine
self.game_engine.grid_click(grid_x, grid_y)
if self.game_engine.game_state == STATE.CHARACTER_SCREEN:
self.handle_character_screen_click(x, y)
[docs] def on_draw(self):
"""
Render the screen.
"""
arcade.start_render()
self.draw_sprites_and_status_panel()
if self.game_engine.game_state == STATE.NORMAL:
self.draw_in_normal_state()
elif self.game_engine.game_state == STATE.SELECT_LOCATION:
self.draw_in_select_location_state()
elif self.game_engine.game_state == STATE.CHARACTER_SCREEN:
self.draw_character_screen()
[docs] def on_key_press(self, key: int, modifiers: int):
"""
Manage key-down events
:param key:
:param modifiers:
"""
# Clear the timer for auto-repeat of movement
self.time_since_last_move_check = None
if key in KEYMAP.UP:
self.up_pressed = True
elif key in KEYMAP.CHARACTER_SCREEN:
self.game_engine.game_state = STATE.CHARACTER_SCREEN
print("Open character screen")
elif key in KEYMAP.CANCEL:
self.game_engine.game_state = STATE.NORMAL
# Movement
elif key in KEYMAP.DOWN:
self.down_pressed = True
elif key in KEYMAP.LEFT:
self.left_pressed = True
elif key in KEYMAP.RIGHT:
self.right_pressed = True
elif key in KEYMAP.UP_LEFT:
self.up_left_pressed = True
elif key in KEYMAP.UP_RIGHT:
self.up_right_pressed = True
elif key in KEYMAP.DOWN_LEFT:
self.down_left_pressed = True
elif key in KEYMAP.DOWN_RIGHT:
self.down_right_pressed = True
# Item management
elif key in KEYMAP.PICKUP:
self.game_engine.action_queue.extend([{"pickup": True}])
elif key in KEYMAP.DROP_ITEM:
self.game_engine.action_queue.extend([{"drop_item": True}])
elif key in KEYMAP.SELECT_ITEM_1:
self.game_engine.action_queue.extend([{"select_item": 1}])
elif key in KEYMAP.SELECT_ITEM_2:
self.game_engine.action_queue.extend([{"select_item": 2}])
elif key in KEYMAP.SELECT_ITEM_3:
self.game_engine.action_queue.extend([{"select_item": 3}])
elif key in KEYMAP.SELECT_ITEM_4:
self.game_engine.action_queue.extend([{"select_item": 4}])
elif key in KEYMAP.SELECT_ITEM_5:
self.game_engine.action_queue.extend([{"select_item": 5}])
elif key in KEYMAP.SELECT_ITEM_6:
self.game_engine.action_queue.extend([{"select_item": 6}])
elif key in KEYMAP.SELECT_ITEM_7:
self.game_engine.action_queue.extend([{"select_item": 7}])
elif key in KEYMAP.SELECT_ITEM_8:
self.game_engine.action_queue.extend([{"select_item": 8}])
elif key in KEYMAP.SELECT_ITEM_9:
self.game_engine.action_queue.extend([{"select_item": 9}])
elif key in KEYMAP.SELECT_ITEM_0:
self.game_engine.action_queue.extend([{"select_item": 0}])
elif key in KEYMAP.USE_ITEM:
self.game_engine.action_queue.extend([{"use_item": True}])
# Save/load
elif key == arcade.key.S:
self.save()
elif key == arcade.key.L:
self.load()
elif key in KEYMAP.USE_STAIRS:
self.game_engine.action_queue.extend([{"use_stairs": True}])
[docs] def on_key_release(self, key: int, modifiers: int):
"""
Called when the user releases a key.
:param key:
:param modifiers:
"""
if key in KEYMAP.UP:
self.up_pressed = False
elif key in KEYMAP.DOWN:
self.down_pressed = False
elif key in KEYMAP.LEFT:
self.left_pressed = False
elif key in KEYMAP.RIGHT:
self.right_pressed = False
elif key in KEYMAP.UP_LEFT:
self.up_left_pressed = False
elif key in KEYMAP.UP_RIGHT:
self.up_right_pressed = False
elif key in KEYMAP.DOWN_LEFT:
self.down_left_pressed = False
elif key in KEYMAP.DOWN_RIGHT:
self.down_right_pressed = False
[docs] def on_mouse_motion(self, x: float, y: float, dx: float, dy: float):
""" Handle mouse motion, mostly just used for mouse-over text. """
# Get current mouse position. Used elsewhere when we need it.
self.mouse_position = x, y
# Get the sprites at the current location
sprite_list = arcade.get_sprites_at_point((x, y), self.game_engine.cur_level.creatures)
# See if any sprite we are hovering over deserves a mouse-over text
self.mouse_over_text = None
for sprite in sprite_list:
if isinstance(sprite, Entity):
if sprite.fighter and sprite.is_visible:
self.mouse_over_text = (
f"{sprite.name} {sprite.fighter.hp}/{sprite.fighter.max_hp}"
)
else:
raise TypeError("Sprite is not an instance of Entity class.")
[docs] def save(self):
""" Save the current game to disk. """
game_dict = self.game_engine.get_dict()
with open("game_save.json", "w") as write_file:
json.dump(game_dict, write_file, indent=4, sort_keys=True)
results = [{"message": "Game has been saved"}]
self.game_engine.action_queue.extend(results)
[docs] def load(self):
""" Load the game from disk. """
with open("game_save.json", "r") as read_file:
data = json.load(read_file)
self.game_engine.restore_from_dict(data)
[docs] def check_for_player_movement(self):
"""
Figure out if we should move the player or not based on keys currently
held down.
"""
# Player is dead, don't move her.
if self.game_engine.player.is_dead:
return
# Reset the movement clock used for holding the key down for repeated movement.
self.time_since_last_move_check = 0
# cx and cy are the delta in movement. Start with no movement.
cx = 0
cy = 0
# Adjust delta of movement based on keys pressed
if self.up_pressed or self.up_left_pressed or self.up_right_pressed:
cy += 1
if self.down_pressed or self.down_left_pressed or self.down_right_pressed:
cy -= 1
if self.left_pressed or self.down_left_pressed or self.up_left_pressed:
cx -= 1
if self.right_pressed or self.down_right_pressed or self.up_right_pressed:
cx += 1
# If we are trying to move, pass that request to the game_engine
if cx or cy:
self.game_engine.move_player(cx, cy)
[docs] def on_update(self, delta_time: float):
"""
Manage regular updates for the game
:param delta_time:
"""
# --- Manage continuous movement while direction keys are held down
# Time since last check, if we are tracking
if self.time_since_last_move_check is not None:
self.time_since_last_move_check += delta_time
# Check if we should move again based on the clock, or if the clock
# was set to None as a trigger to move immediate
if (
self.time_since_last_move_check is None
or self.time_since_last_move_check >= REPEAT_MOVEMENT_DELAY
):
self.check_for_player_movement()
# --- Process the action queue
self.game_engine.process_action_queue(delta_time)
self.game_engine.check_experience_level()
[docs]def main():
""" Main method for starting the rogue-like game """
window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
window.setup()
arcade.run()