diff --git a/__pycache__/end.cpython-313.pyc b/__pycache__/end.cpython-313.pyc new file mode 100644 index 0000000..5b26228 Binary files /dev/null and b/__pycache__/end.cpython-313.pyc differ diff --git a/__pycache__/intro_scene.cpython-313.pyc b/__pycache__/intro_scene.cpython-313.pyc index 3bdf2c7..cc71830 100644 Binary files a/__pycache__/intro_scene.cpython-313.pyc and b/__pycache__/intro_scene.cpython-313.pyc differ diff --git a/__pycache__/level_data.cpython-313.pyc b/__pycache__/level_data.cpython-313.pyc index 1d152cc..c1d40ac 100644 Binary files a/__pycache__/level_data.cpython-313.pyc and b/__pycache__/level_data.cpython-313.pyc differ diff --git a/__pycache__/scenes.cpython-313.pyc b/__pycache__/scenes.cpython-313.pyc index c7604bc..8e75b17 100644 Binary files a/__pycache__/scenes.cpython-313.pyc and b/__pycache__/scenes.cpython-313.pyc differ diff --git a/__pycache__/tiles.cpython-313.pyc b/__pycache__/tiles.cpython-313.pyc index 1d7d83c..0a3c9fc 100644 Binary files a/__pycache__/tiles.cpython-313.pyc and b/__pycache__/tiles.cpython-313.pyc differ diff --git a/assets/audio/ambient_loop.mpeg b/assets/audio/ambient_loop.mpeg new file mode 100644 index 0000000..617a4a0 Binary files /dev/null and b/assets/audio/ambient_loop.mpeg differ diff --git a/assets/audio/collect.mpeg b/assets/audio/collect.mpeg new file mode 100644 index 0000000..441277b Binary files /dev/null and b/assets/audio/collect.mpeg differ diff --git a/assets/audio/win.mpeg b/assets/audio/win.mpeg new file mode 100644 index 0000000..ecca050 Binary files /dev/null and b/assets/audio/win.mpeg differ diff --git a/assets/end/dodo.png b/assets/end/dodo.png new file mode 100644 index 0000000..ceca0cf Binary files /dev/null and b/assets/end/dodo.png differ diff --git a/assets/fundal.jpg b/assets/fundal.jpg new file mode 100644 index 0000000..37dd0ef Binary files /dev/null and b/assets/fundal.jpg differ diff --git a/assets/intro/mixkit-hard-typewriter-click-1119.wav b/assets/intro/mixkit-hard-typewriter-click-1119.wav new file mode 100644 index 0000000..e0fa946 Binary files /dev/null and b/assets/intro/mixkit-hard-typewriter-click-1119.wav differ diff --git a/end.py b/end.py new file mode 100644 index 0000000..5398363 --- /dev/null +++ b/end.py @@ -0,0 +1,66 @@ +import pygame +from pathlib import Path + + +class EndScene: + def __init__(self, manager, assets, on_exit, collected_count: int, required_count: int): + self.manager = manager + self.assets = assets + self.on_exit = on_exit + self.collected_count = collected_count + self.required_count = required_count + + self.big_font = pygame.font.SysFont(None, 72) + self.mid_font = pygame.font.SysFont(None, 36) + self.small_font = pygame.font.SysFont(None, 28) + + self.end_art = None + art_path = Path(__file__).resolve().parent / "assets/end/dodo.png" + try: + self.end_art = pygame.image.load(str(art_path)).convert_alpha() + except Exception: + self.end_art = None + + try: + pygame.mixer.music.set_volume(0.08) + except Exception: + pass + + + def handle_event(self, event): + if event.type == pygame.KEYDOWN and event.key in (pygame.K_RETURN, pygame.K_SPACE, pygame.K_ESCAPE): + self.on_exit() + + def update(self, dt): + pass + + def draw(self, screen): + if self.end_art is not None: + bg = pygame.transform.smoothscale(self.end_art, screen.get_size()) + screen.blit(bg, (0, 0)) + elif getattr(self.assets, "background_img", None): + bg = pygame.transform.smoothscale(self.assets.background_img, screen.get_size()) + screen.blit(bg, (0, 0)) + else: + screen.fill((12, 12, 18)) + + overlay = pygame.Surface(screen.get_size(), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 145)) + screen.blit(overlay, (0, 0)) + + title = self.big_font.render("THE END", True, (120, 240, 140)) + line1 = self.mid_font.render("You entered the green portal.", True, (235, 235, 235)) + line2 = self.small_font.render("Press ENTER / SPACE / ESC to return to HUB", True, (200, 200, 200)) + count = self.small_font.render( + f"Collected: {self.collected_count}/{self.required_count}", + True, + (120, 240, 220), + ) + + cx = screen.get_width() // 2 + cy = screen.get_height() // 2 + + screen.blit(title, (cx - title.get_width() // 2, cy - 96)) + screen.blit(line1, (cx - line1.get_width() // 2, cy - 22)) + screen.blit(count, (cx - count.get_width() // 2, cy + 20)) + screen.blit(line2, (cx - line2.get_width() // 2, cy + 72)) diff --git a/intro_scene.py b/intro_scene.py index 2d05a06..147a5c5 100644 --- a/intro_scene.py +++ b/intro_scene.py @@ -13,22 +13,26 @@ TEXT_PADDING = 20 FONT_SIZE = 32 -TYPING_SPEED_FRAMES = 6 +TYPING_SPEED_FRAMES = 8 +SFX_CHAR_INTERVAL = 1 class TypewriterText: - def __init__(self, full_text: str, font: pygame.font.Font, max_width: int, speed_frames: int): + def __init__(self, full_text: str, font: pygame.font.Font, max_width: int, speed_frames: int, typing_sfx: pygame.mixer.Sound | None = None): self.full_text = full_text or "" self.font = font self.max_width = max_width self.speed = max(1, speed_frames) + self.typing_sfx = typing_sfx self.frame_counter = 0 self.done = (self.full_text == "") self.lines = self._wrap_text(self.full_text) + self.flat_text = "".join(self.lines) self.visible_chars = 0 - self.total_chars = sum(len(line) for line in self.lines) + self.total_chars = len(self.flat_text) + self.sfx_counter = 0 def _wrap_text(self, text: str): if not text: @@ -54,7 +58,17 @@ def update(self): self.frame_counter += 1 if self.frame_counter >= self.speed: self.frame_counter = 0 - self.visible_chars += 1 + if self.visible_chars < self.total_chars: + ch = self.flat_text[self.visible_chars] + self.visible_chars += 1 + if self.typing_sfx is not None: + self.sfx_counter += 1 + if self.sfx_counter >= SFX_CHAR_INTERVAL: + self.sfx_counter = 0 + try: + self.typing_sfx.play() + except Exception: + pass if self.visible_chars >= self.total_chars: self.visible_chars = self.total_chars self.done = True @@ -197,6 +211,7 @@ def __init__(self, manager, game_assets, hub_scene_ctor, intro_dir: Path): pygame.font.init() self.font = pygame.font.SysFont("monospace", FONT_SIZE) self.prompt_font = pygame.font.SysFont("monospace", 18) + self.typing_sfx = self._load_typing_sfx() self.script = [ ("frame1.jpg", "The Dodo was always a suspicious bird, so the caveman wanted to follow it"), @@ -209,11 +224,45 @@ def __init__(self, manager, game_assets, hub_scene_ctor, intro_dir: Path): self.frames = [] for fname, text in self.script: bg = _load_and_fit_bg(self.intro_dir / fname) - tw = TypewriterText(text, self.font, TEXT_BOX_W - TEXT_PADDING * 2, TYPING_SPEED_FRAMES) + tw = TypewriterText(text, self.font, TEXT_BOX_W - TEXT_PADDING * 2, TYPING_SPEED_FRAMES, self.typing_sfx) self.frames.append({"bg": bg, "tw": tw, "is_last": (fname == "frame5.jpg")}) self.index = 0 + + def _load_typing_sfx(self): + audio_dir = self.intro_dir.parent / "audio" + project_dir = self.intro_dir.parent.parent + + candidates = [ + project_dir / "mixkit-hard-typewriter-click-1119.wav", + self.intro_dir / "mixkit-hard-typewriter-click-1119.wav", + audio_dir / "mixkit-hard-typewriter-click-1119.wav", + ] + + for path in candidates: + if not path.exists(): + continue + try: + s = pygame.mixer.Sound(str(path)) + s.set_volume(0.35) + return s + except Exception: + continue + + for stem in ("typewriter", "typing", "keyboard", "keys", "collect"): + for ext in (".wav", ".ogg", ".mp3", ".mpeg"): + path = audio_dir / f"{stem}{ext}" + if not path.exists(): + continue + try: + s = pygame.mixer.Sound(str(path)) + s.set_volume(0.35) + return s + except Exception: + continue + return None + def _cur(self): return self.frames[self.index] diff --git a/level_data.py b/level_data.py index c07602e..ecba3dc 100644 --- a/level_data.py +++ b/level_data.py @@ -1,6 +1,10 @@ """ -Maps generated directly from HARTI.xlsx. -Each string is exactly 80 chars wide, and each map has 45 rows. +Updated cave maps for Finding Dodo. +HUB adjusted to match the cave layout request: +- left shaft filled with solid rock +- HUB spikes removed +- portal to LEVEL1 moved to upper corridor +- portal to LEVEL2 moved to middle-right corridor """ WIDTH = 80 @@ -33,24 +37,24 @@ "################################################################################", "################################################################################", "...............................................#################################", + ".......P.......................................#################################", "...............................................#################################", - "...PPPPP.................................111111#################################", - "...PPPPP.................................111111#################################", - "...PPPPP.................................111111#################################", + ".........................................11....#################################", + ".........................................11....#################################", "....############....############################################################", - "....############...................................................#############", - "....############...................................................#############", - "....############...................................................#############", - "....####################...........................................#############", - "....####################.....................................222222#############", - "....####################.....................................222222#############", - "....####################.....................................222222#############", - "....###~~~~~~~~~#######################....#####################################", - "....###~~~~~~~~~~######################.......................^^^^............##", - "....#~~~~~~~~~~~~######################.........................^.........FFFF##", - "....#~~~~~~~~~~########################...................................FFFF##", - "....###############################################~~~~#########################", - "....################################################~###########################", + "################...................................................#############", + "################...................................................#############", + "################...................................................#############", + "########################...........................................#############", + "########################...........................................#############", + "########################......................................22...#############", + "########################......................................22...#############", + "#######################################....#####################################", + "#######################################.......................................##", + "#######################################...................................FFFF##", + "#######################################...................................FFFF##", + "################################################################################", + "################################################################################", "....############################################################################", "....############################################################################", "....############################################################################", @@ -72,8 +76,8 @@ "###############################################################################.", "###############################################################################.", "############################################.......##############^^############.", - "..#############....^^^^#####################.......############..^^############.", - "..#############.....^^.#####################.......############...^.###########.", + "..#############......^^#####################.......############..^^..##########.", + "..#############........#####################.......############...^..##########.", "..#############........#####...................CCC.###..........................", "..#############........#####...................CCC.###..........................", "..#######..............#####...................CCC.###.............######....RR.", @@ -118,30 +122,30 @@ LEVEL2 = [ "################################################################################", - "############################################################..........##########", + "###########.......##########################################..........##########", "..............................^^^^^.........................................####", "....PP.........................^^^.......................................CC.####", "....PP...................................#############.........####......CC.####", - "....########~~~~###########..............####################~~~~~~~~###########", - "....#########~#############.........##########################~~~~~~############", - "....#######################.........###########################~~~##############", - "....##############..................############################################", - "....##############...........############^^^##########....................######", - "....#######..................############^^^##########....................######", - "....#######.................#############.^.##########....................######", - "....####...........######################...####..........................######", + "############~~~~###########..............####################~~~~~~~~###########", + "#############~#############.........##########################~~~~~~############", + "###########################.........###########################~~~##############", + "##################..................############################################", + "##################...........############^^^##########....................######", + "###########..................############^^^##########....................######", + "###########.................#############.^.##########....................######", + "########...........######################...####..........................######", "....####...........##########...............####........................CC######", "....##......#################...........................................CC######", "....##....#########.................................................#~~#########", "....##....########..................#############################...#~~#########", "....##....########..................##############################..#~~~########", "....##....########.................###############################..###~########", - "....##....########..............##############^^.........#########..############", - "....##....########..............#########.....^^.............#####..############", - "....##....########..............#########..CC.^...............####..############", + "....##....########..............##############...........#########..############", + "....##....########..............#########....................#####..############", + "....##....########..............#########..CC.................####..############", "....##....####.................##########..CC.....#~~~##......####..############", "....##....####...............#################..###~~~#####...####..########^^..", - "....##....##.................#################..#~~~#######..####...########^^..", + "....##....##.................#################..#~~~######...####...########^^..", "....##....##................##################.##~~###.......####...########^...", "....##....##................##########################......#####...............", "....##....##................#######################.......#######...............", @@ -150,16 +154,16 @@ "....##....##.......#######..........#######............#.......##...............", "....##....##.......#######..........#####...........######.....##.............RR", "....##.....#.......#######..........#####...........######.....##.............RR", - "....##..........##############..............##~~~~##########...........#~~~#####", - "....##..........########.#####..............###~############...........##~######", + "....##..........##############..............##~~~~##########...........###~#####", + "....##..........########.#####..............###~############...........#########", "....##..........########.#####..............####~###########.........###########", - "....##..........######...#####......#############################....###########", - "....####..############...#####......#############################.....##########", - "....####..############CC.^^#######..#############################.....##########", - "....##....############CC.^.#######..#############################.....##########", - "....##..vv############.......#####..###########################.......##########", - "....##..vv###############....#####..###########################.......##########", - "....##vvvv###############.........................................vv..##########", + "....##..........######....####......#############################....###########", + "....####..############....####......#############################.....##########", + "....####..############.....#######..#############################.....##########", + "....##....############CC....######..#############################.....##########", + "....##..vv############cc......####..###########################.......##########", + "....##..vv###############.......##..###########################.......##########", + "....##vvvv###############.............................................##########", "....##vvvv###############..###...................................vvvv.##########", "....##vvvv######################################################################", ] diff --git a/main.py b/main.py index a463601..a1662ae 100644 --- a/main.py +++ b/main.py @@ -1,165 +1,139 @@ import pygame from pathlib import Path -import copy -import level_data from scenes import Assets, HubScene from intro_scene import IntroScene, MenuScene -# ===================== INTERNAL RESOLUTION ===================== -TILE = 16 -W_TILES, H_TILES = 80, 45 -INTERNAL_W = W_TILES * TILE -INTERNAL_H = H_TILES * TILE +# ===================== WINDOW ===================== +INTERNAL_W = 1280 +INTERNAL_H = 720 FPS = 60 -DOUBLE_ESC_WINDOW_S = 0.5 +TITLE = "Finding Dodo" +# ===================== ASSETS ===================== +BASE_DIR = Path(__file__).resolve().parent +TILESET_PATH = BASE_DIR / "assets/tileset.png" +PLAYER_PATH = BASE_DIR / "assets/player.png" +INTRO_DIR = BASE_DIR / "assets/intro" -def calc_viewport(win_w: int, win_h: int) -> pygame.Rect: - scale = min(win_w / INTERNAL_W, win_h / INTERNAL_H) - vw = max(1, int(INTERNAL_W * scale)) - vh = max(1, int(INTERNAL_H * scale)) - x = (win_w - vw) // 2 - y = (win_h - vh) // 2 - return pygame.Rect(x, y, vw, vh) - - -def make_window(resizable=True, fullscreen=False): - flags = 0 - if fullscreen: - flags |= pygame.FULLSCREEN - return pygame.display.set_mode((0, 0), flags) - if resizable: - flags |= pygame.RESIZABLE - return pygame.display.set_mode((1280, 720), flags) +# ===================== AUDIO ===================== +AUDIO_DIR = BASE_DIR / "assets/audio" +AMBIENT_PATH = AUDIO_DIR / "ambient_loop.mpeg" +AMBIENT_VOLUME = 0.18 class SceneManager: - def __init__(self, game_assets, intro_dir: Path): - self.scene = None - self.viewport = pygame.Rect(0, 0, INTERNAL_W, INTERNAL_H) - self.window_size = (1280, 720) - - self.game_assets = game_assets - self.intro_dir = Path(intro_dir) + def __init__(self, window: pygame.Surface, internal_surface: pygame.Surface, assets: Assets): + self.window = window + self.internal_surface = internal_surface + self.assets = assets - # Double ESC tracking - self._last_esc_t = None + self.scene = None - # Dynamic difficulty state - self.level2_lava_stage = 0 + # global progress for collectibles + self.collected_count = 0 + self.collected_ids = set() - # Base maps (deep copies) - self.base_hub = copy.deepcopy(level_data.HUB) - self.base_l1 = copy.deepcopy(level_data.LEVEL1) - self.base_l2 = copy.deepcopy(level_data.LEVEL2) + # optional hook used by scenes.py + self.level2_deaths = 0 def change(self, new_scene): self.scene = new_scene - def set_viewport(self, viewport: pygame.Rect, window_size): - self.viewport = viewport - self.window_size = window_size + def start_intro(self): + self.change(IntroScene(self, self.assets, HubScene, INTRO_DIR)) - def window_to_internal(self, pos): - x, y = pos - vp = self.viewport - if not vp.collidepoint(x, y): - return None - ix = int((x - vp.x) * (INTERNAL_W / vp.w)) - iy = int((y - vp.y) * (INTERNAL_H / vp.h)) - return (ix, iy) - - def mouse_internal(self): - ip = self.window_to_internal(pygame.mouse.get_pos()) - return ip if ip is not None else (-9999, -9999) - - # ===== menu / game control ===== def go_menu(self): - # reset maps/difficulty - self.level2_lava_stage = 0 - self.change(MenuScene(self, self.game_assets, HubScene, self.intro_dir)) + self.change(MenuScene(self, self.assets, HubScene, INTRO_DIR)) def start_game(self): - # reset difficulty + restart hub - self.level2_lava_stage = 0 - self.change(HubScene(self, self.game_assets)) - - # ===== global double-esc ===== - def handle_global_event(self, event) -> bool: - if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: - now = pygame.time.get_ticks() / 1000.0 - if self._last_esc_t is not None and (now - self._last_esc_t) <= DOUBLE_ESC_WINDOW_S: - self._last_esc_t = None - self.go_menu() - return True # consume second ESC - self._last_esc_t = now - return False - - # ===== dynamic level grids ===== + self.change(HubScene(self, self.assets)) + + def on_level2_death(self): + self.level2_deaths += 1 + def get_level_grid(self, level_id: int): - # always return a COPY (so scenes can modify safely if needed) + import level_data + if level_id == 1: - return copy.deepcopy(self.base_l1) + return level_data.LEVEL1 if level_id == 2: - grid = copy.deepcopy(self.base_l2) - return self._apply_level2_progressive_lava(grid, self.level2_lava_stage) - return copy.deepcopy(self.base_hub) + return level_data.LEVEL2 + if level_id == 3: + return level_data.LEVEL3 + return level_data.LEVEL1 - def on_level2_death(self): - self.level2_lava_stage += 1 + def window_to_internal(self, pos): + wx, wy = self.window.get_size() + scale = min(wx / INTERNAL_W, wy / INTERNAL_H) - def _apply_level2_progressive_lava(self, grid, stage: int): - """ - Adds more lava each time you die in Level2. - Adds strips near bottom, avoiding spawn (x~2) and door (x~40 on H-3). - """ - if stage <= 0: - return grid + draw_w = int(INTERNAL_W * scale) + draw_h = int(INTERNAL_H * scale) + off_x = (wx - draw_w) // 2 + off_y = (wy - draw_h) // 2 - W = len(grid[0]) - H = len(grid) + mx, my = pos + if not (off_x <= mx < off_x + draw_w and off_y <= my < off_y + draw_h): + return None + + ix = int((mx - off_x) / scale) + iy = int((my - off_y) / scale) + return ix, iy + + def mouse_internal(self): + pos = pygame.mouse.get_pos() + converted = self.window_to_internal(pos) + if converted is None: + return -9999, -9999 + return converted - # place lava on rows above ground - # ground expected on last row - lava_rows = [H - 4, H - 5] # two rows of lava pool - start_x = 52 - length = min(8 + stage * 6, W - start_x - 2) - for y in lava_rows: - if 0 <= y < H - 1: - row = list(grid[y]) - for x in range(start_x, start_x + length): - if 0 <= x < W and row[x] == '.': - row[x] = '~' - grid[y] = "".join(row) +def draw_scaled(window: pygame.Surface, internal_surface: pygame.Surface): + wx, wy = window.get_size() + scale = min(wx / INTERNAL_W, wy / INTERNAL_H) - return grid + draw_w = int(INTERNAL_W * scale) + draw_h = int(INTERNAL_H * scale) + off_x = (wx - draw_w) // 2 + off_y = (wy - draw_h) // 2 + + window.fill((0, 0, 0)) + scaled = pygame.transform.smoothscale(internal_surface, (draw_w, draw_h)) + window.blit(scaled, (off_x, off_y)) + + +def start_ambient_music(): + try: + if pygame.mixer.get_init() is None: + pygame.mixer.init() + pygame.mixer.set_num_channels(16) + + if AMBIENT_PATH.exists(): + pygame.mixer.music.load(str(AMBIENT_PATH)) + pygame.mixer.music.set_volume(AMBIENT_VOLUME) + pygame.mixer.music.play(-1) + except Exception: + pass def main(): pygame.init() - pygame.display.set_caption("GAMEJAME") - - base = Path(__file__).resolve().parent - tileset_path = base / "assets" / "tileset.png" - player_path = base / "assets" / "player.png" - intro_dir = base / "assets" / "intro" - fullscreen = False - window = make_window(resizable=True, fullscreen=fullscreen) - win_w, win_h = window.get_size() + window = pygame.display.set_mode((INTERNAL_W, INTERNAL_H), pygame.RESIZABLE) + pygame.display.set_caption(TITLE) + internal_surface = pygame.Surface((INTERNAL_W, INTERNAL_H)) clock = pygame.time.Clock() - render = pygame.Surface((INTERNAL_W, INTERNAL_H)).convert_alpha() - game_assets = Assets.from_files(str(tileset_path), str(player_path)) - manager = SceneManager(game_assets, intro_dir) - manager.set_viewport(calc_viewport(win_w, win_h), (win_w, win_h)) + start_ambient_music() + + assets = Assets.from_files(str(TILESET_PATH), str(PLAYER_PATH), BASE_DIR) + manager = SceneManager(window, internal_surface, assets) + manager.start_intro() - # Start with Intro frames - manager.change(IntroScene(manager, game_assets, HubScene, intro_dir)) + esc_last_time = 0 + esc_window_ms = 400 running = True while running: @@ -168,35 +142,34 @@ def main(): for event in pygame.event.get(): if event.type == pygame.QUIT: running = False + break - if event.type == pygame.KEYDOWN and event.key == pygame.K_F11: - fullscreen = not fullscreen - window = make_window(resizable=True, fullscreen=fullscreen) - win_w, win_h = window.get_size() - manager.set_viewport(calc_viewport(win_w, win_h), (win_w, win_h)) + if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: + now = pygame.time.get_ticks() - if event.type == pygame.VIDEORESIZE and not fullscreen: - win_w, win_h = event.w, event.h - window = pygame.display.set_mode((win_w, win_h), pygame.RESIZABLE) - manager.set_viewport(calc_viewport(win_w, win_h), (win_w, win_h)) + # double ESC -> menu + if now - esc_last_time <= esc_window_ms: + try: + pygame.mixer.music.set_volume(AMBIENT_VOLUME) + except Exception: + pass + manager.go_menu() + esc_last_time = 0 + continue - # Global double-ESC -> menu - consumed = manager.handle_global_event(event) - if consumed: - continue + esc_last_time = now - manager.scene.handle_event(event) + if manager.scene is not None: + manager.scene.handle_event(event) - manager.scene.update(dt) + if manager.scene is not None: + manager.scene.update(dt) - render.fill((12, 12, 20)) - manager.scene.draw(render) + internal_surface.fill((0, 0, 0)) + manager.scene.draw(internal_surface) - vp = manager.viewport - scaled = pygame.transform.scale(render, (vp.w, vp.h)) - window.fill((0, 0, 0)) - window.blit(scaled, vp.topleft) - pygame.display.flip() + draw_scaled(window, internal_surface) + pygame.display.flip() pygame.quit() diff --git a/mixkit-hard-typewriter-click-1119.wav b/mixkit-hard-typewriter-click-1119.wav new file mode 100644 index 0000000..e0fa946 Binary files /dev/null and b/mixkit-hard-typewriter-click-1119.wav differ diff --git a/scenes.py b/scenes.py index a75d33d..1b5b14d 100644 --- a/scenes.py +++ b/scenes.py @@ -1,7 +1,9 @@ import pygame from dataclasses import dataclass +from pathlib import Path from tiles import TileSet, load_player, TILE +from end import EndScene # ===================== PHYSICS ===================== GRAVITY = 0.75 @@ -19,30 +21,125 @@ # ===================== DOOR SAFE ZONE ===================== SAFE_ZONE_TILES = 2 -# ===================== STALACT/STALAG sizes (jumpable) ===================== +# ===================== COLLECTIBLES ===================== +CONSUMABLE_CHAR = "C" +REQUIRED_CONSUMABLES = 6 + +# ===================== STALACT/STALAG ===================== STALACT_LEN_TILES = 3 STALAG_LEN_TILES = 3 STALACT_FALL_GRAV = 0.9 -STALACT_MAX_FALL = 20.0 +STALACT_MAX_FALL = 20.0 STALACT_TRIGGER_PAD = 2 TIP_H = TILE TIP_W = max(4, TILE // 3) +# ===================== COLORS ===================== +SOLID_FILL = (101, 67, 33) # maro +SOLID_OUTLINE = (55, 35, 18) # contur maro inchis +PLATFORM_FILL = (125, 85, 45) +PLATFORM_OUTLINE = (65, 40, 20) + @dataclass class Assets: tileset: TileSet player_img: pygame.Surface + collect_sfx: pygame.mixer.Sound | None + win_sfx: pygame.mixer.Sound | None + background_img: pygame.Surface | None @staticmethod - def from_files(tileset_path: str, player_path: str) -> "Assets": + def from_files(tileset_path: str, player_path: str, base_dir: Path | None = None) -> "Assets": + root = Path(base_dir) if base_dir is not None else Path(__file__).resolve().parent + audio_dir = root / "assets/audio" + + def load_sfx(stem: str, volume: float): + for ext in (".wav", ".ogg", ".mp3", ".mpeg"): + path = audio_dir / f"{stem}{ext}" + if not path.exists(): + continue + try: + s = pygame.mixer.Sound(str(path)) + s.set_volume(volume) + return s + except Exception: + continue + return None + + def load_background(): + try: + return pygame.image.load(str(root / "assets/fundal.jpg")).convert() + except Exception: + return None + return Assets( tileset=TileSet(tileset_path), player_img=load_player(player_path), + collect_sfx=load_sfx("collect", 0.45), + win_sfx=load_sfx("win", 0.60), + background_img=load_background(), ) +def build_connected_groups(points): + pts = set(points) + groups = [] + + while pts: + start = next(iter(pts)) + stack = [start] + pts.remove(start) + group = [] + + while stack: + x, y = stack.pop() + group.append((x, y)) + + for nx, ny in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + if (nx, ny) in pts: + pts.remove((nx, ny)) + stack.append((nx, ny)) + + groups.append(group) + + return groups + + +class CollectibleGroup: + def __init__(self, level_id: int, tiles: list[tuple[int, int]]): + self.level_id = level_id + self.tiles = sorted(tiles) + + self.rects = [ + pygame.Rect(x * TILE, y * TILE, TILE, TILE) + for x, y in self.tiles + ] + + min_x = min(x for x, _ in self.tiles) * TILE + min_y = min(y for _, y in self.tiles) * TILE + max_x = (max(x for x, _ in self.tiles) + 1) * TILE + max_y = (max(y for _, y in self.tiles) + 1) * TILE + self.bounds = pygame.Rect(min_x, min_y, max_x - min_x, max_y - min_y) + + self.uid = (self.level_id, tuple(self.tiles)) + + def touches(self, player_rect: pygame.Rect) -> bool: + for r in self.rects: + if player_rect.colliderect(r): + return True + return False + + def draw(self, screen): + outer = self.bounds.inflate(-max(2, TILE // 6), -max(2, TILE // 6)) + pygame.draw.rect(screen, (0, 180, 180), outer, border_radius=6) + + inner = outer.inflate(-max(4, TILE // 5), -max(4, TILE // 5)) + if inner.width > 0 and inner.height > 0: + pygame.draw.rect(screen, (80, 255, 255), inner, border_radius=6) + + class TileMap: def __init__(self, grid): self.grid = grid @@ -55,6 +152,7 @@ def __init__(self, grid): self.base_solids: list[pygame.Rect] = [] self.doors: list[pygame.Rect] = [] self.lava: list[pygame.Rect] = [] + self.collectible_tiles: list[tuple[int, int]] = [] self.spawn_px = (2 * TILE, (self.h - 3) * TILE) self.stalact_anchors: list[tuple[int, int]] = [] @@ -66,6 +164,7 @@ def _build(self): self.base_solids.clear() self.doors.clear() self.lava.clear() + self.collectible_tiles.clear() self.stalact_anchors.clear() self.stalag_bases.clear() @@ -76,7 +175,7 @@ def _build(self): if ch in ("#", "="): self.base_solids.append(r) - elif ch == "D": + elif ch in ("D", "1", "2", "R", "F"): self.doors.append(r) elif ch == "P": self.spawn_px = (rx, ry) @@ -86,26 +185,81 @@ def _build(self): self.stalag_bases.append((x, y)) elif ch == LAVA_CHAR: self.lava.append(r) + elif ch == CONSUMABLE_CHAR: + self.collectible_tiles.append((x, y)) - # cage self.base_solids.extend([ pygame.Rect(-TILE, 0, TILE, self.world_h), pygame.Rect(self.world_w, 0, TILE, self.world_h), pygame.Rect(0, -TILE, self.world_w, TILE), ]) + def tile_at_pixel(self, px: int, py: int) -> str: + tx = px // TILE + ty = py // TILE + if 0 <= ty < self.h and 0 <= tx < self.w: + return self.grid[ty][tx] + return "." + + def tile_under_player(self, player_rect: pygame.Rect) -> str: + return self.tile_at_pixel(player_rect.centerx, player_rect.centery) + + def interaction_tile(self, player_rect: pygame.Rect) -> str: + # Robust portal interaction: sample center/feet/sides and overlap area. + hits = set() + + sample_points = [ + (player_rect.centerx, player_rect.centery), + (player_rect.centerx, player_rect.bottom - 2), + (player_rect.left + 4, player_rect.centery), + (player_rect.right - 4, player_rect.centery), + ] + for px, py in sample_points: + ch = self.tile_at_pixel(px, py) + if ch in ("D", "1", "2", "R", "F"): + hits.add(ch) + + tx0 = max(0, player_rect.left // TILE) + ty0 = max(0, player_rect.top // TILE) + tx1 = min(self.w - 1, player_rect.right // TILE) + ty1 = min(self.h - 1, player_rect.bottom // TILE) + + for ty in range(ty0, ty1 + 1): + row = self.grid[ty] + for tx in range(tx0, tx1 + 1): + ch = row[tx] + if ch in ("D", "1", "2", "R", "F"): + hits.add(ch) + + for ch in ("F", "D", "R", "1", "2"): + if ch in hits: + return ch + return "." + def draw(self, screen, assets: Assets): for y, row in enumerate(self.grid): for x, ch in enumerate(row): px, py = x * TILE, y * TILE + rect = pygame.Rect(px, py, TILE, TILE) + if ch == "#": - screen.blit(assets.tileset.ground, (px, py)) + pygame.draw.rect(screen, SOLID_FILL, rect) + pygame.draw.rect(screen, SOLID_OUTLINE, rect, 2) elif ch == "=": - screen.blit(assets.tileset.platform, (px, py)) + pygame.draw.rect(screen, PLATFORM_FILL, rect) + pygame.draw.rect(screen, PLATFORM_OUTLINE, rect, 2) elif ch == "D": - pygame.draw.rect(screen, (180, 70, 70), pygame.Rect(px, py, TILE, TILE)) + pygame.draw.rect(screen, (180, 70, 70), rect) + elif ch == "1": + pygame.draw.rect(screen, (60, 220, 120), rect) + elif ch == "2": + pygame.draw.rect(screen, (255, 170, 40), rect) + elif ch == "R": + pygame.draw.rect(screen, (80, 140, 255), rect) + elif ch == "F": + pygame.draw.rect(screen, (80, 200, 80), rect) elif ch == LAVA_CHAR: - pygame.draw.rect(screen, (220, 80, 20), pygame.Rect(px, py, TILE, TILE)) + pygame.draw.rect(screen, (220, 80, 20), rect) class StalactiteGroup: @@ -166,15 +320,31 @@ def update(self, solids_static, player_rect): def draw(self, screen): base_h = 2 * TILE - pygame.draw.rect(screen, (95, 95, 95), pygame.Rect(self.x, int(self.y), self.w, min(base_h, self.h))) - pygame.draw.rect(screen, (75, 75, 75), pygame.Rect(self.x, int(self.y) + min(base_h, self.h), self.w, max(0, self.h - base_h))) + pygame.draw.rect( + screen, + (95, 95, 95), + pygame.Rect(self.x, int(self.y), self.w, min(base_h, self.h)) + ) + pygame.draw.rect( + screen, + (75, 75, 75), + pygame.Rect( + self.x, + int(self.y) + min(base_h, self.h), + self.w, + max(0, self.h - base_h) + ) + ) bottom = int(self.y) + self.h for i in range(self.w_tiles): col_x = self.x + i * TILE cx = col_x + TILE // 2 - pygame.draw.polygon(screen, (55, 55, 55), - [(cx, bottom), (cx - TILE // 2, bottom - TILE), (cx + TILE // 2, bottom - TILE)]) + pygame.draw.polygon( + screen, + (55, 55, 55), + [(cx, bottom), (cx - TILE // 2, bottom - TILE), (cx + TILE // 2, bottom - TILE)] + ) class StalagmiteGroup: @@ -205,17 +375,31 @@ def tip_hitboxes(self): def draw(self, screen): base_h = 2 * TILE - pygame.draw.rect(screen, (95, 95, 95), - pygame.Rect(self.x, int(self.y) + self.h - min(base_h, self.h), self.w, min(base_h, self.h))) - pygame.draw.rect(screen, (75, 75, 75), - pygame.Rect(self.x, int(self.y), self.w, max(0, self.h - base_h))) + pygame.draw.rect( + screen, + (95, 95, 95), + pygame.Rect( + self.x, + int(self.y) + self.h - min(base_h, self.h), + self.w, + min(base_h, self.h) + ) + ) + pygame.draw.rect( + screen, + (75, 75, 75), + pygame.Rect(self.x, int(self.y), self.w, max(0, self.h - base_h)) + ) top = int(self.y) for i in range(self.w_tiles): col_x = self.x + i * TILE cx = col_x + TILE // 2 - pygame.draw.polygon(screen, (55, 55, 55), - [(cx, top), (cx - TILE // 2, top + TILE), (cx + TILE // 2, top + TILE)]) + pygame.draw.polygon( + screen, + (55, 55, 55), + [(cx, top), (cx - TILE // 2, top + TILE), (cx + TILE // 2, top + TILE)] + ) class Player: @@ -293,6 +477,7 @@ def build_contiguous_runs(points): runs[y] = row_runs return runs + def build_stalactites(tilemap): if not tilemap.stalact_anchors: return [] @@ -303,6 +488,7 @@ def build_stalactites(tilemap): out.append(StalactiteGroup(y, x0, x1)) return out + def build_stalagmites(tilemap): if not tilemap.stalag_bases: return [] @@ -313,6 +499,7 @@ def build_stalagmites(tilemap): out.append(StalagmiteGroup(y, x0, x1)) return out + def in_door_safe_zone(player_rect, doors): inflate = SAFE_ZONE_TILES * TILE for d in doors: @@ -320,6 +507,7 @@ def in_door_safe_zone(player_rect, doors): return True return False + def build_solids(tilemap, stalactites, stalagmites, safe): solids = list(tilemap.base_solids) if safe: @@ -328,6 +516,7 @@ def build_solids(tilemap, stalactites, stalagmites, safe): solids.extend([s.solid_rect() for s in stalagmites]) return solids + def collect_lethal_tips(stalactites, stalagmites, safe): if safe: return [] @@ -338,6 +527,7 @@ def collect_lethal_tips(stalactites, stalagmites, safe): tips.extend(s.tip_hitboxes()) return tips + def clamp_player(player, tilemap): if player.rect.left < 0: player.rect.left = 0 @@ -347,12 +537,53 @@ def clamp_player(player, tilemap): player.rect.top = 0 -class HubScene: +def ensure_global_progress(manager): + if not hasattr(manager, "collected_count"): + manager.collected_count = 0 + if not hasattr(manager, "collected_ids"): + manager.collected_ids = set() + + +def reset_global_progress(manager): + manager.collected_count = 0 + manager.collected_ids = set() + + +def exit_to_hub_from_end(manager, assets): + reset_global_progress(manager) + try: + pygame.mixer.music.set_volume(0.18) + except Exception: + pass + manager.change(HubScene(manager, assets)) + + +class BaseScene: + def draw_background(self, screen): + if self.assets.background_img: + bg = self.assets.background_img + if bg.get_size() != screen.get_size(): + bg = pygame.transform.smoothscale(bg, screen.get_size()) + screen.blit(bg, (0, 0)) + else: + screen.fill((0, 0, 0)) + + def draw_counter(self, screen): + ensure_global_progress(self.manager) + font = pygame.font.SysFont(None, 28) + text = f"{self.manager.collected_count}/{REQUIRED_CONSUMABLES}" + surf = font.render(text, True, (230, 230, 230)) + screen.blit(surf, (screen.get_width() - surf.get_width() - 12, 10)) + + +class HubScene(BaseScene): def __init__(self, manager, assets): self.manager = manager self.assets = assets + ensure_global_progress(self.manager) import level_data + self.map = TileMap(level_data.HUB) self.player = Player(self.map.spawn_px, assets.player_img) self.font = pygame.font.SysFont(None, 24) @@ -360,8 +591,6 @@ def __init__(self, manager, assets): self.stalactites = build_stalactites(self.map) self.stalagmites = build_stalagmites(self.map) - self.door_targets = {0: 1, 1: 2} - self.pending_reset = False self.death_timer = 0.0 @@ -378,6 +607,7 @@ def schedule_reset(self, delay): self.death_timer = delay def do_reset(self): + reset_global_progress(self.manager) self.manager.change(HubScene(self.manager, self.assets)) def start_lava_stun(self): @@ -404,7 +634,6 @@ def update(self, dt): keys = pygame.key.get_pressed() - # lava touch for lr in self.map.lava: if self.player.rect.colliderect(lr): self.start_lava_stun() @@ -427,36 +656,71 @@ def update(self, dt): clamp_player(self.player, self.map) - if keys[pygame.K_e]: - for idx, d in enumerate(self.map.doors): - if self.player.rect.colliderect(d): - lvl = self.door_targets.get(idx) - self.manager.change(LevelScene(self.manager, self.assets, lvl)) - break + if keys[pygame.K_e] or keys[pygame.K_i]: + current_tile = self.map.interaction_tile(self.player.rect) + + if current_tile == "1": + self.manager.change(LevelScene(self.manager, self.assets, 1)) + return + elif current_tile == "2": + self.manager.change(LevelScene(self.manager, self.assets, 2)) + return + elif current_tile == "F": + self.manager.change( + EndScene( + self.manager, + self.assets, + lambda: exit_to_hub_from_end(self.manager, self.assets), + self.manager.collected_count, + REQUIRED_CONSUMABLES, + ) + ) + return def draw(self, screen): + self.draw_background(screen) self.map.draw(screen, self.assets) + for sg in self.stalagmites: sg.draw(screen) for st in self.stalactites: st.draw(screen) + screen.blit(self.player.image, self.player.rect) - screen.blit(self.font.render("HUB: E pe usa | ESCx2 -> MENU", True, (230, 230, 230)), (10, 10)) + screen.blit( + self.font.render("HUB: E/I pe usa | ESCx2 -> MENU", True, (230, 230, 230)), + (10, 10) + ) + self.draw_counter(screen) + if self.lava_stun: - screen.blit(self.font.render("STUCK IN LAVA...", True, (255, 200, 120)), (10, 30)) + screen.blit( + self.font.render("STUCK IN LAVA...", True, (255, 200, 120)), + (10, 30) + ) elif self.pending_reset: - screen.blit(self.font.render("DEAD...", True, (255, 180, 180)), (10, 30)) + screen.blit( + self.font.render("DEAD...", True, (255, 180, 180)), + (10, 30) + ) -class LevelScene: +class LevelScene(BaseScene): def __init__(self, manager, assets, level_id: int): self.manager = manager self.assets = assets self.level_id = level_id + ensure_global_progress(self.manager) grid = self.manager.get_level_grid(level_id) self.map = TileMap(grid) + self.collectibles: list[CollectibleGroup] = [] + for group_tiles in build_connected_groups(self.map.collectible_tiles): + grp = CollectibleGroup(self.level_id, group_tiles) + if grp.uid not in self.manager.collected_ids: + self.collectibles.append(grp) + self.player = Player(self.map.spawn_px, assets.player_img) self.font = pygame.font.SysFont(None, 24) @@ -470,7 +734,6 @@ def __init__(self, manager, assets, level_id: int): self.lava_timer = 0.0 def handle_event(self, event): - # single ESC can still go to hub, but double ESC is global in main.py if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: self.manager.change(HubScene(self.manager, self.assets)) @@ -481,9 +744,11 @@ def schedule_reset(self, delay): self.death_timer = delay def do_reset(self): - # If Level2, next run should have more lava - if self.level_id == 2: + reset_global_progress(self.manager) + + if self.level_id == 2 and hasattr(self.manager, "on_level2_death"): self.manager.on_level2_death() + self.manager.change(LevelScene(self.manager, self.assets, self.level_id)) def start_lava_stun(self): @@ -494,6 +759,19 @@ def start_lava_stun(self): self.player.vx = 0.0 self.player.vy = 0.0 + def collect_consumables(self): + remaining = [] + for grp in self.collectibles: + if grp.touches(self.player.rect): + if grp.uid not in self.manager.collected_ids: + self.manager.collected_ids.add(grp.uid) + self.manager.collected_count += 1 + if self.assets.collect_sfx: + self.assets.collect_sfx.play() + else: + remaining.append(grp) + self.collectibles = remaining + def update(self, dt): if self.lava_stun: self.lava_timer -= dt @@ -532,22 +810,114 @@ def update(self, dt): clamp_player(self.player, self.map) - # door back to hub - if keys[pygame.K_e]: - for d in self.map.doors: - if self.player.rect.colliderect(d): + self.collect_consumables() + + if keys[pygame.K_e] or keys[pygame.K_i]: + current_tile = self.map.interaction_tile(self.player.rect) + + if current_tile in ("D", "R"): + if self.manager.collected_count >= REQUIRED_CONSUMABLES: + self.manager.change(WinScene(self.manager, self.assets)) + else: self.manager.change(HubScene(self.manager, self.assets)) - break + return + + if current_tile == "F": + self.manager.change( + EndScene( + self.manager, + self.assets, + lambda: exit_to_hub_from_end(self.manager, self.assets), + self.manager.collected_count, + REQUIRED_CONSUMABLES, + ) + ) + return def draw(self, screen): + self.draw_background(screen) self.map.draw(screen, self.assets) + + for grp in self.collectibles: + grp.draw(screen) + for sg in self.stalagmites: sg.draw(screen) for st in self.stalactites: st.draw(screen) + screen.blit(self.player.image, self.player.rect) - screen.blit(self.font.render(f"LEVEL {self.level_id}: ESC->HUB | ESCx2->MENU", True, (230, 230, 230)), (10, 10)) + screen.blit( + self.font.render( + f"LEVEL {self.level_id}: E/I interact | ESC->HUB | ESCx2->MENU", + True, + (230, 230, 230) + ), + (10, 10) + ) + self.draw_counter(screen) + if self.lava_stun: - screen.blit(self.font.render("STUCK IN LAVA...", True, (255, 200, 120)), (10, 30)) + screen.blit( + self.font.render("STUCK IN LAVA...", True, (255, 200, 120)), + (10, 30) + ) elif self.pending_reset: - screen.blit(self.font.render("DEAD...", True, (255, 180, 180)), (10, 30)) \ No newline at end of file + screen.blit( + self.font.render("DEAD...", True, (255, 180, 180)), + (10, 30) + ) + + +class WinScene(BaseScene): + def __init__(self, manager, assets): + self.manager = manager + self.assets = assets + ensure_global_progress(self.manager) + + self.big_font = pygame.font.SysFont(None, 72) + self.mid_font = pygame.font.SysFont(None, 36) + self.small_font = pygame.font.SysFont(None, 28) + + try: + pygame.mixer.music.set_volume(0.08) + except Exception: + pass + + + def handle_event(self, event): + if event.type == pygame.KEYDOWN: + if event.key in (pygame.K_RETURN, pygame.K_SPACE, pygame.K_ESCAPE): + reset_global_progress(self.manager) + try: + pygame.mixer.music.set_volume(0.18) + except Exception: + pass + self.manager.change(HubScene(self.manager, self.assets)) + + def update(self, dt): + pass + + def draw(self, screen): + self.draw_background(screen) + + overlay = pygame.Surface(screen.get_size(), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 160)) + screen.blit(overlay, (0, 0)) + + title = self.big_font.render("YOU WIN", True, (240, 240, 120)) + line1 = self.mid_font.render("All consumables collected.", True, (220, 220, 220)) + line2 = self.small_font.render("Press ENTER / SPACE / ESC to return to HUB", True, (180, 180, 180)) + count = self.small_font.render( + f"Collected: {self.manager.collected_count}/{REQUIRED_CONSUMABLES}", + True, + (120, 240, 220) + ) + + cx = screen.get_width() // 2 + cy = screen.get_height() // 2 + + screen.blit(title, (cx - title.get_width() // 2, cy - 90)) + screen.blit(line1, (cx - line1.get_width() // 2, cy - 20)) + screen.blit(count, (cx - count.get_width() // 2, cy + 20)) + screen.blit(line2, (cx - line2.get_width() // 2, cy + 70)) \ No newline at end of file diff --git a/tiles.py b/tiles.py index af91397..0f6f7d9 100644 --- a/tiles.py +++ b/tiles.py @@ -1,49 +1,49 @@ -import pygame - -TILE = 16 - -def load_image(path: str) -> pygame.Surface: - return pygame.image.load(path).convert_alpha() - -def crop(sheet: pygame.Surface, x: int, y: int, w: int, h: int) -> pygame.Surface: - return sheet.subsurface(pygame.Rect(x, y, w, h)).copy() - -def scale_nearest(img: pygame.Surface, w: int, h: int) -> pygame.Surface: - # pentru pixel-art (fara blur) - return pygame.transform.scale(img, (w, h)) - -class TileSet: - """ - Folosește tileset-ul tău și extrage: - - ground (verde mic) - - platform (maro mic) - - Coordonate (din imaginea ta): - maro mic: x=48, y=0, w=32, h=32 - verde mic: x=48, y=48, w=32, h=32 - """ - def __init__(self, tileset_path: str): - sheet = load_image(tileset_path) - - brown_32 = crop(sheet, 48, 0, 32, 32) # platform - green_32 = crop(sheet, 48, 48, 32, 32) # ground - - # le aducem la 16x16 (rezolutie interna) - self.platform = scale_nearest(brown_32, TILE, TILE) - self.ground = scale_nearest(green_32, TILE, TILE) - -def load_player(player_path: str) -> pygame.Surface: - """ - Player 2x mai mare: 32x32 (intern). - Dacă player.png nu există, face fallback. - """ - PLAYER_SCALE = 2 # <-- 2x - - try: - img = load_image(player_path) - img = pygame.transform.scale(img, (TILE, TILE)) # îl tratăm ca 16x16 - except Exception: - img = pygame.Surface((TILE, TILE), pygame.SRCALPHA) - img.fill((220, 180, 60)) - +import pygame + +TILE = 16 + +def load_image(path: str) -> pygame.Surface: + return pygame.image.load(path).convert_alpha() + +def crop(sheet: pygame.Surface, x: int, y: int, w: int, h: int) -> pygame.Surface: + return sheet.subsurface(pygame.Rect(x, y, w, h)).copy() + +def scale_nearest(img: pygame.Surface, w: int, h: int) -> pygame.Surface: + # pentru pixel-art (fara blur) + return pygame.transform.scale(img, (w, h)) + +class TileSet: + """ + Folosește tileset-ul tău și extrage: + - ground (verde mic) + - platform (maro mic) + + Coordonate (din imaginea ta): + maro mic: x=48, y=0, w=32, h=32 + verde mic: x=48, y=48, w=32, h=32 + """ + def __init__(self, tileset_path: str): + sheet = load_image(tileset_path) + + brown_32 = crop(sheet, 48, 0, 32, 32) # platform + green_32 = crop(sheet, 48, 48, 32, 32) # ground + + # le aducem la 16x16 (rezolutie interna) + self.platform = scale_nearest(brown_32, TILE, TILE) + self.ground = scale_nearest(green_32, TILE, TILE) + +def load_player(player_path: str) -> pygame.Surface: + """ + Player 2x mai mare: 32x32 (intern). + Dacă player.png nu există, face fallback. + """ + PLAYER_SCALE = 2 # <-- 2x + + try: + img = load_image(player_path) + img = pygame.transform.scale(img, (TILE, TILE)) # îl tratăm ca 16x16 + except Exception: + img = pygame.Surface((TILE, TILE), pygame.SRCALPHA) + img.fill((220, 180, 60)) + return pygame.transform.scale(img, (TILE * PLAYER_SCALE, TILE * PLAYER_SCALE)) \ No newline at end of file