Merge branch 'main' into feat/items
# Conflicts: # src/settings.py
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
|
from src.game import Game
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("Hello from silent-fh-squad-g!")
|
game = Game()
|
||||||
|
game.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
"""Small playable demo that uses ``src.map.Map`` and the actor system.
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
- WASD / arrow keys: move Knoll around the map
|
||||||
|
- Space: switch between good/normal and bad/angry sprite
|
||||||
|
- Esc / Q: quit
|
||||||
|
|
||||||
|
The demo also places a few bad/angry actors as monsters in the rooms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
from src.actors import Actor
|
||||||
|
from src.map import Map, color_void, screen_h, screen_w, tile_size
|
||||||
|
from src.settings import DEMO_ACTOR_HEIGHT, FPS, PLAYER_SPEED
|
||||||
|
from src.startscreen import Startscreen
|
||||||
|
|
||||||
|
|
||||||
|
# Keep the actor close to the map scale. The map tiles are 30x30, so 2 tiles
|
||||||
|
# high makes the character readable without covering whole rooms/walls.
|
||||||
|
|
||||||
|
|
||||||
|
def tile_center(column: int, row: int) -> tuple[int, int]:
|
||||||
|
"""Return the pixel center of a map tile."""
|
||||||
|
|
||||||
|
return column * tile_size + tile_size // 2, row * tile_size + tile_size // 2
|
||||||
|
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
pygame.init()
|
||||||
|
self.screen = pygame.display.set_mode((screen_w, screen_h))
|
||||||
|
self.clock = pygame.time.Clock()
|
||||||
|
self.font = pygame.font.SysFont("monospace", 22, bold=True)
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
self.startscreen = Startscreen(self.screen)
|
||||||
|
self.state = "startscreen"
|
||||||
|
self.player_name: str = ""
|
||||||
|
|
||||||
|
self.map = Map()
|
||||||
|
|
||||||
|
# resolve the assets from the folder
|
||||||
|
assets_dir = Path(__file__).resolve().parent.parent / "assets"
|
||||||
|
actor_sides = Actor.load_sprite_sides(assets_dir, target_height=DEMO_ACTOR_HEIGHT)
|
||||||
|
|
||||||
|
if not actor_sides:
|
||||||
|
raise RuntimeError("No normal_*/angry_* actor sprite pairs found in assets/")
|
||||||
|
|
||||||
|
# Use the first pair for the player. With current assets this is Knoll.
|
||||||
|
self.player = Actor.from_sides(actor_sides[0], self.find_start_position())
|
||||||
|
|
||||||
|
def run(self, max_frames: Optional[int] = None) -> None:
|
||||||
|
"""Run the demo. ``max_frames`` is only used for automated smoke tests."""
|
||||||
|
|
||||||
|
frame_count = 0
|
||||||
|
while self.running and (max_frames is None or frame_count < max_frames):
|
||||||
|
dt = min(self.clock.tick(FPS) / 1000.0, 0.05)
|
||||||
|
|
||||||
|
if self.state == "startscreen":
|
||||||
|
next_state = self.startscreen.run()
|
||||||
|
if next_state == "quit":
|
||||||
|
self.running = False
|
||||||
|
elif next_state == "game":
|
||||||
|
self.player_name = self.startscreen.player_name
|
||||||
|
self.state = "game"
|
||||||
|
elif self.state == "game":
|
||||||
|
self.handle_events()
|
||||||
|
self.update(dt)
|
||||||
|
self.draw()
|
||||||
|
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
frame_count += 1
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
|
|
||||||
|
def handle_events(self) -> None:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
self.running = False
|
||||||
|
elif event.type == pygame.KEYDOWN:
|
||||||
|
if event.key in (pygame.K_ESCAPE, pygame.K_q):
|
||||||
|
self.running = False
|
||||||
|
elif event.key == pygame.K_SPACE:
|
||||||
|
self.player.toggle_side()
|
||||||
|
|
||||||
|
def update(self, dt: float) -> None:
|
||||||
|
movement = self.get_player_movement(dt)
|
||||||
|
self.player.move_by(movement)
|
||||||
|
|
||||||
|
if not self.is_position_walkable(self.player.position):
|
||||||
|
self.player.hit_wall()
|
||||||
|
|
||||||
|
self.player.update_animation(dt)
|
||||||
|
|
||||||
|
def get_player_movement(self, dt: float) -> pygame.Vector2:
|
||||||
|
keys = pygame.key.get_pressed()
|
||||||
|
movement = pygame.Vector2(
|
||||||
|
(keys[pygame.K_RIGHT] or keys[pygame.K_d])
|
||||||
|
- (keys[pygame.K_LEFT] or keys[pygame.K_a]),
|
||||||
|
(keys[pygame.K_DOWN] or keys[pygame.K_s])
|
||||||
|
- (keys[pygame.K_UP] or keys[pygame.K_w]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if movement.length_squared() == 0:
|
||||||
|
return movement
|
||||||
|
|
||||||
|
if abs(movement.x) > abs(movement.y):
|
||||||
|
self.player.direction = "right" if movement.x > 0 else "left"
|
||||||
|
else:
|
||||||
|
self.player.direction = "down" if movement.y > 0 else "up"
|
||||||
|
|
||||||
|
return movement.normalize() * PLAYER_SPEED * dt
|
||||||
|
|
||||||
|
def is_position_walkable(self, position: pygame.Vector2) -> bool:
|
||||||
|
"""Check if the actor's feet are standing on a walkable map tile."""
|
||||||
|
|
||||||
|
column = int(position.x // tile_size)
|
||||||
|
row = int(position.y // tile_size)
|
||||||
|
return self.map.is_accessible(column, row)
|
||||||
|
|
||||||
|
def find_start_position(self) -> tuple[int, int]:
|
||||||
|
"""Find the first S tile and place the actor's feet near its center."""
|
||||||
|
|
||||||
|
for row_index, row in enumerate(self.map.map):
|
||||||
|
column_index = row.find("S")
|
||||||
|
if column_index != -1:
|
||||||
|
# The S tile is at the very top of the current map, so place the
|
||||||
|
# actor one tile lower to keep the sprite visible on screen.
|
||||||
|
return (
|
||||||
|
column_index * tile_size + tile_size // 2,
|
||||||
|
(row_index + 1) * tile_size + tile_size // 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tile_size, tile_size
|
||||||
|
|
||||||
|
def draw(self) -> None:
|
||||||
|
self.screen.fill(color_void)
|
||||||
|
self.map.draw_map(self.screen, self.font)
|
||||||
|
|
||||||
|
|
||||||
|
# Draw by Y position so actors lower on the map appear in front.
|
||||||
|
for actor in sorted([self.player], key=lambda item: item.position.y):
|
||||||
|
actor.draw(self.screen)
|
||||||
|
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
import pygame
|
||||||
|
from src.settings import (
|
||||||
|
HUD_FONT, HUD_FONT_SIZE, HUD_FONT_BOLD,
|
||||||
|
COLOR_HUD_TEXT, COLOR_HP_BAR_GREEN, COLOR_HP_BAR_RED, COLOR_HP_BAR_BG,
|
||||||
|
HUD_BAR_WIDTH, HUD_BAR_HEIGHT,
|
||||||
|
HUD_X_OFFSET, HUD_BAR_X, HUD_Y_DOOR, HUD_Y_ZOMBIE, HUD_Y_ECTS
|
||||||
|
)
|
||||||
|
|
||||||
|
# draw status bar (background bar and the colored hp bar)
|
||||||
|
def _draw_status_bar(screen, x, y, current, max_val, color):
|
||||||
|
# bg bar is always the same
|
||||||
|
pygame.draw.rect(screen, COLOR_HP_BAR_BG, (x, y, HUD_BAR_WIDTH, HUD_BAR_HEIGHT))
|
||||||
|
|
||||||
|
# hp bar
|
||||||
|
if max_val > 0:
|
||||||
|
hp_width = (current / max_val) * HUD_BAR_WIDTH
|
||||||
|
pygame.draw.rect((screen, color), (x, y, hp_width, HUD_BAR_HEIGHT))
|
||||||
|
|
||||||
|
# time formatter for MM:SS
|
||||||
|
def _format_time(total_seconds):
|
||||||
|
mins = total_seconds // 60
|
||||||
|
secs = total_seconds % 60
|
||||||
|
return f"TIME {mins:02}:{secs:02}"
|
||||||
|
|
||||||
|
|
||||||
|
class HUD:
|
||||||
|
def __init__(self):
|
||||||
|
self.font = pygame.font.SysFont(HUD_FONT, HUD_FONT_SIZE, bold=HUD_FONT_BOLD)
|
||||||
|
|
||||||
|
def draw(self, screen, door, zombie, ects, total_time):
|
||||||
|
# --- DOOR HP ---
|
||||||
|
_draw_status_bar(screen, HUD_BAR_X, HUD_Y_DOOR, door.current_hp, COLOR_HP_BAR_GREEN)
|
||||||
|
# -5 px, small off-set to align the text better with hp bar
|
||||||
|
screen.blit(self.font.render("DOOR HP ", True, COLOR_HUD_TEXT), (HUD_X_OFFSET, HUD_Y_DOOR - 5))
|
||||||
|
|
||||||
|
# --- ZOMBIE HP ---
|
||||||
|
if zombie:
|
||||||
|
_draw_status_bar(screen, HUD_BAR_X, HUD_Y_ZOMBIE, zombie.current_hp, zombie.max_hp, COLOR_HP_BAR_RED)
|
||||||
|
screen.blit(self.font.render("Zombie HP:", True, COLOR_HUD_TEXT), (HUD_X_OFFSET, HUD_Y_ZOMBIE - 5))
|
||||||
|
|
||||||
|
# --- ECTS ---
|
||||||
|
ects_info = f"ECTS: {ects.ects_balance} ({ects.ects_per_second}/s)"
|
||||||
|
screen.blit(self.font.render(ects_info, True, COLOR_HUD_TEXT), (HUD_X_OFFSET, HUD_Y_ECTS))
|
||||||
|
|
||||||
|
# --- TIME ---
|
||||||
|
time_str = _format_time(total_time)
|
||||||
|
screen.blit(self.font.render(time_str, True, COLOR_HUD_TEXT), (screen.get_width() - 150, HUD_Y_DOOR - 5))
|
||||||
|
|||||||
+15
-2
@@ -1,6 +1,8 @@
|
|||||||
# --- DESK ---
|
# --- DESK ---
|
||||||
from typing import Mapping, Literal, Tuple
|
from typing import Mapping, Literal, Tuple
|
||||||
|
|
||||||
|
from src.map import tile_size
|
||||||
|
|
||||||
DESK_WIDTH = 70
|
DESK_WIDTH = 70
|
||||||
DESK_HEIGHT = 40
|
DESK_HEIGHT = 40
|
||||||
|
|
||||||
@@ -12,7 +14,8 @@ DOOR_BORDER_WIDTH = 3
|
|||||||
|
|
||||||
COLOR_DOOR_BEIGE = (245, 245, 220)
|
COLOR_DOOR_BEIGE = (245, 245, 220)
|
||||||
COLOR_DOOR_BORDER = (100, 90, 80)
|
COLOR_DOOR_BORDER = (100, 90, 80)
|
||||||
|
COLOR_HP_BAR_GREEN = (0, 255, 0)
|
||||||
|
COLOR_HP_BAR_BG = (50, 50, 50)
|
||||||
|
|
||||||
# Players Constants / Settings
|
# Players Constants / Settings
|
||||||
# Valid movement/animation directions.
|
# Valid movement/animation directions.
|
||||||
@@ -33,7 +36,6 @@ DIRECTION_FRAMES: Mapping[Direction, Tuple[int, int, int]] = {
|
|||||||
DEFAULT_SPRITE_HEIGHT: int = 138
|
DEFAULT_SPRITE_HEIGHT: int = 138
|
||||||
DEFAULT_FRAME_TIME: float = 0.16
|
DEFAULT_FRAME_TIME: float = 0.16
|
||||||
|
|
||||||
|
|
||||||
# --- ECTS ---
|
# --- ECTS ---
|
||||||
START_ECTS_VALUE = 1 # how many ECTS are generated at the beginning
|
START_ECTS_VALUE = 1 # how many ECTS are generated at the beginning
|
||||||
ECTS_UPDATE_INTERVAL = 1000 # time in ms (1000 = 1 sec)
|
ECTS_UPDATE_INTERVAL = 1000 # time in ms (1000 = 1 sec)
|
||||||
@@ -98,3 +100,14 @@ COLOR_HP_BAR_YELLOW = (255, 255, 0)
|
|||||||
|
|
||||||
HUD_BAR_WIDTH = 100
|
HUD_BAR_WIDTH = 100
|
||||||
HUD_BAR_HEIGHT = 15
|
HUD_BAR_HEIGHT = 15
|
||||||
|
|
||||||
|
PLAYER_SPEED = 180.0
|
||||||
|
DEMO_ACTOR_HEIGHT = tile_size * 3
|
||||||
|
FPS = 60
|
||||||
|
|
||||||
|
HUD_X_OFFSET = 20 # left startpoint for texts
|
||||||
|
HUD_BAR_X = 150 # left startpoint for hp bars
|
||||||
|
|
||||||
|
HUD_Y_DOOR = 20
|
||||||
|
HUD_Y_ZOMBIE = 50
|
||||||
|
HUD_Y_ECTS = 85
|
||||||
|
|||||||
+6
-4
@@ -1,11 +1,13 @@
|
|||||||
import pygame
|
import pygame
|
||||||
from button import Button
|
from src.button import Button
|
||||||
from settings import (COLOR_BUTTON,
|
from src.settings import (
|
||||||
|
COLOR_BUTTON,
|
||||||
COLOR_BUTTON_HOVER,
|
COLOR_BUTTON_HOVER,
|
||||||
COLOR_STARTSCREEN_BACKGROUND,
|
COLOR_STARTSCREEN_BACKGROUND,
|
||||||
COLOR_STARTSCREEN_FONT_TITLE,
|
COLOR_STARTSCREEN_FONT_TITLE,
|
||||||
COLOR_STARTSCREEN_FONT_LABEL)
|
COLOR_STARTSCREEN_FONT_LABEL,
|
||||||
from text_input import TextInput
|
)
|
||||||
|
from src.text_input import TextInput
|
||||||
|
|
||||||
class Startscreen:
|
class Startscreen:
|
||||||
def __init__(self, screen):
|
def __init__(self, screen):
|
||||||
|
|||||||
Reference in New Issue
Block a user