From df5cd8346d8441912996e3d912db876f4d457c0f Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Sat, 29 Nov 2025 18:39:14 +0100 Subject: [PATCH] fix best time not loading due to not using level num, update README, lock the game to 60 FPS, add hitboxes setting and camera shake --- README.md | 5 ++++- game/play.py | 43 ++++++++++++++++++++++++++++++++++--------- menus/settings.py | 18 ++---------------- run.py | 27 ++------------------------- utils/constants.py | 5 +---- 5 files changed, 43 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index a0c7401..e745174 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -Ember Keeper is a platformer 2D game where you have to escape the outside temperatures, while collecting firewood to keep yourself warm while being out in the open. \ No newline at end of file +Ember Keeper is a platformer 2D game where you have to escape the outside temperatures, while collecting firewood to keep yourself warm while being out in the open. + +The game is locked at 60 FPS since every interaction depends on that and i am lazy to introduce delta time. +The game can be speedrunned and there is an option for hitboxes as well. \ No newline at end of file diff --git a/game/play.py b/game/play.py index fdb2b1a..22fab6b 100644 --- a/game/play.py +++ b/game/play.py @@ -40,6 +40,14 @@ class Game(arcade.gui.UIView): walls=[self.scene["walls"], self.scene["ice"]] ) + self.camera_shake = arcade.camera.grips.ScreenShake2D( + self.camera_sprites.view_data, + max_amplitude=5, + acceleration_duration=0.2, + falloff_time=0.3, + shake_frequency=10.0, + ) + self.warmth = 50 self.direction = "right" self.last_jump = time.perf_counter() @@ -50,6 +58,7 @@ class Game(arcade.gui.UIView): self.restart_start = time.perf_counter() self.restarting = False self.won = False + self.won_time = None self.level_texts = [] @@ -74,7 +83,7 @@ class Game(arcade.gui.UIView): for level_num in range(AVAILABLE_LEVELS) }) - self.best_time = self.data.get("best_time", 9999) + self.best_time = self.data.get(f"{self.level_num}_best_time", 9999) self.tries = self.data.get("tries", 1) if self.best_time == 9999: self.no_besttime = True @@ -96,6 +105,8 @@ class Game(arcade.gui.UIView): self.info_label = self.anchor.add(arcade.gui.UILabel(text=f"Time took: 0s Best Time: {self.best_time}s Trees: 0 Tries: {self.tries}", font_size=20), anchor_x="center", anchor_y="top") def reset(self, reached_end=False): + self.camera_shake.start() + if not reached_end: self.warmth = 50 self.trees = 0 @@ -115,7 +126,7 @@ class Game(arcade.gui.UIView): if self.no_besttime: self.no_besttime = False - self.anchor.add(arcade.gui.UILabel(text=f"Level Complete! Time: {round(time.perf_counter() - self.start, 2)}s\nBest Time: {self.best_time}", multiline=True, font_size=30), anchor_x="center", anchor_y="center") + self.anchor.add(arcade.gui.UILabel(text=f"Level Complete! Time: {self.won_time}s\nBest Time: {self.best_time}s", multiline=True, font_size=30), anchor_x="center", anchor_y="center") self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) self.back_button.on_click = lambda event: self.main_exit() @@ -144,16 +155,23 @@ class Game(arcade.gui.UIView): def on_draw(self): self.clear() + self.camera_shake.update_camera() + if not self.won: with self.camera_sprites.activate(): self.scene.draw() + if self.settings.get("hitboxes", False): + self.scene.draw_hit_boxes(arcade.color.RED, 2) + for level_text in self.level_texts: level_text.draw() arcade.draw_lbwh_rectangle_filled(self.window.width / 4, 0, (self.window.width / 2), self.window.height / 20, arcade.color.SKY_BLUE) arcade.draw_lbwh_rectangle_filled(self.window.width / 4, 0, (self.window.width / 2) * (self.warmth / 100), self.window.height / 20, arcade.color.RED) + self.camera_shake.readjust_camera() + self.ui.draw() def center_camera_to_player(self): @@ -187,28 +205,33 @@ class Game(arcade.gui.UIView): hit_list = self.physics_engine.update() self.center_camera_to_player() + self.camera_shake.update(delta_time) if self.player.collides_with_list(self.scene["end"]): - end_time = round(time.perf_counter() - self.start, 2) + end_time = round(time.perf_counter() - self.start, 4) if self.no_besttime or end_time < self.best_time: self.best_time = end_time + + self.won_time = end_time self.reset(True) return if self.no_besttime: - self.best_time = round(time.perf_counter() - self.start, 2) + self.best_time = round(time.perf_counter() - self.start, 4) - self.info_label.text = f"Time took: {round(time.perf_counter() - self.start, 2)}s Best Time: {self.best_time}s Trees: {self.trees} Tries: {self.tries}" + self.info_label.text = f"Time took: {round(time.perf_counter() - self.start, 4)}s Best Time: {self.best_time}s Trees: {self.trees} Tries: {self.tries}" if self.warmth <= 0 or self.player.collides_with_list(self.scene["spikes"]) or self.player.center_y < 0: self.reset() if self.player.center_x + self.player.width / 2 < 0: + self.camera_shake.start() self.player.center_x = self.player.width / 2 if self.player.center_x - self.player.width / 2 > tilemaps[self.level_num].width * GRID_PIXEL_SIZE: + self.camera_shake.start() self.player.center_x = (tilemaps[self.level_num].width * GRID_PIXEL_SIZE) - self.player.width / 2 for tree in self.player.collides_with_list(self.scene["trees"]): @@ -222,7 +245,7 @@ class Game(arcade.gui.UIView): if checkpoint not in self.checkpoints_hit: self.scene["checkpoints"].remove(checkpoint) self.checkpoints_hit.add(checkpoint) - self.spawn_position = checkpoint.position + arcade.math.Vec2(-GRID_PIXEL_SIZE / 8, GRID_PIXEL_SIZE / 2) + self.spawn_position = checkpoint.position + arcade.math.Vec2(-GRID_PIXEL_SIZE / 4, GRID_PIXEL_SIZE) moved = False ice_touch = any([ice_sprite in hit_list for ice_sprite in self.scene["ice"]]) and self.physics_engine.can_jump() @@ -248,9 +271,9 @@ class Game(arcade.gui.UIView): on_right_left_diagonal = any([True for hit in hit_list if hit in self.right_left_diagonal_sprites]) if on_left_right_diagonal or (self.direction == "right" and not on_right_left_diagonal): - self.player.change_x = self.clamp(self.player.change_x * 0.75, PLAYER_MOVEMENT_SPEED * 0.4, PLAYER_MOVEMENT_SPEED) + self.player.change_x = self.clamp(self.player.change_x * 0.75, PLAYER_MOVEMENT_SPEED * 0.3, PLAYER_MOVEMENT_SPEED) else: - self.player.change_x = self.clamp(self.player.change_x * 0.75, -PLAYER_MOVEMENT_SPEED, -PLAYER_MOVEMENT_SPEED * 0.4) + self.player.change_x = self.clamp(self.player.change_x * 0.75, -PLAYER_MOVEMENT_SPEED, -PLAYER_MOVEMENT_SPEED * 0.3) else: self.player.change_x = 0 @@ -260,6 +283,7 @@ class Game(arcade.gui.UIView): self.warmth = self.clamp(self.warmth - 0.15, 0, 100) if self.warmth < 40: + self.camera_shake.start() if self.settings.get("sfx", True) and not self.freeze_player.playing: self.freeze_player.play() else: @@ -268,13 +292,14 @@ class Game(arcade.gui.UIView): if self.player.change_y > 0: self.change_player_animation(player_jump_animation) - elif abs(self.player.change_x) > PLAYER_MOVEMENT_SPEED * 0.4: + elif abs(self.player.change_x) > PLAYER_MOVEMENT_SPEED * 0.3: self.change_player_animation(player_walk_animation) else: self.change_player_animation(player_still_animation) for level_text in self.level_texts: if level_text.change_to_when_hit and self.player.rect.intersection(level_text.rect): + self.camera_shake.start() level_text.text = level_text.change_to_when_hit self.player.update_animation() diff --git a/menus/settings.py b/menus/settings.py index 855493d..8f515eb 100644 --- a/menus/settings.py +++ b/menus/settings.py @@ -147,22 +147,8 @@ class Settings(arcade.gui.UIView): width, height = map(int, self.settings_dict['resolution'].split('x')) self.window.set_size(width, height) - if self.settings_dict['vsync']: - self.window.set_vsync(True) - display_mode = self.window.display.get_default_screen().get_mode() - refresh_rate = display_mode.rate - self.window.set_update_rate(1 / refresh_rate) - self.window.set_draw_rate(1 / refresh_rate) - - elif not self.settings_dict['fps_limit'] == 0: - self.window.set_vsync(False) - self.window.set_update_rate(1 / self.settings_dict['fps_limit']) - self.window.set_draw_rate(1 / self.settings_dict['fps_limit']) - - else: - self.window.set_vsync(False) - self.window.set_update_rate(1 / 99999999) - self.window.set_draw_rate(1 / 99999999) + self.window.set_update_rate(1 / 60) + self.window.set_draw_rate(1 / 60) if self.settings_dict['discord_rpc']: if isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session. diff --git a/run.py b/run.py index 36fee4a..ad6247a 100644 --- a/run.py +++ b/run.py @@ -14,13 +14,10 @@ pyglet.font.add_directory(os.path.join(script_dir, 'assets', 'fonts')) from utils.utils import get_closest_resolution, print_debug_info, on_exception from utils.constants import log_dir, menu_background_color from menus.main import Main -# from utils.preload import theme_sound # needed for preload from arcade.experimental.controller_window import ControllerWindow sys.excepthook = on_exception -# __builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args))) - if not log_dir in os.listdir(): os.makedirs(log_dir) @@ -58,7 +55,6 @@ if os.path.exists('settings.json'): fullscreen = settings['window_mode'] == 'Fullscreen' style = arcade.Window.WINDOW_STYLE_BORDERLESS if settings['window_mode'] == 'borderless' else arcade.Window.WINDOW_STYLE_DEFAULT vsync = settings['vsync'] - fps_limit = settings['fps_limit'] else: resolution = get_closest_resolution() antialiasing = 4 @@ -73,7 +69,6 @@ else: fullscreen = False style = arcade.Window.WINDOW_STYLE_DEFAULT vsync = True - fps_limit = 0 settings = { "music": True, @@ -81,38 +76,20 @@ else: "resolution": f"{resolution[0]}x{resolution[1]}", "antialiasing": "4x MSAA", "window_mode": "Windowed", - "vsync": True, - "fps_limit": 60, "discord_rpc": True } with open("settings.json", "w") as file: file.write(json.dumps(settings)) -# if settings.get("music", True): -# theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True) - try: window = ControllerWindow(width=resolution[0], height=resolution[1], title='Ember Keeper', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False) except (FileNotFoundError, PermissionError) as e: logging.warning(f"Controller support unavailable: {e}. Falling back to regular window.") window = arcade.Window(width=resolution[0], height=resolution[1], title='Ember Keeper', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False) -if vsync: - window.set_vsync(True) - display_mode = window.display.get_default_screen().get_mode() - if display_mode: - refresh_rate = display_mode.rate - else: - refresh_rate = 60 - window.set_update_rate(1 / refresh_rate) - window.set_draw_rate(1 / refresh_rate) -elif not fps_limit == 0: - window.set_update_rate(1 / fps_limit) - window.set_draw_rate(1 / fps_limit) -else: - window.set_update_rate(1 / 99999999) - window.set_draw_rate(1 / 99999999) +window.set_update_rate(1 / 60) +window.set_draw_rate(1 / 60) arcade.set_background_color(menu_background_color) diff --git a/utils/constants.py b/utils/constants.py index c230ed6..c5a66bc 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -38,17 +38,14 @@ settings = { "Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"}, "Resolution": {"type": "option", "options": ["1366x768", "1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"}, "Anti-Aliasing": {"type": "option", "options": ["None", "2x MSAA", "4x MSAA", "8x MSAA", "16x MSAA"], "config_key": "anti_aliasing", "default": "4x MSAA"}, - "VSync": {"type": "bool", "config_key": "vsync", "default": True}, - "FPS Limit": {"type": "slider", "min": 0, "max": 480, "config_key": "fps_limit", "default": 60}, }, "Sound": { - "Music": {"type": "bool", "config_key": "music", "default": True}, "SFX": {"type": "bool", "config_key": "sfx", "default": True}, - "Music Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "music_volume", "default": 50}, "SFX Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "sfx_volume", "default": 50}, }, "Miscellaneous": { "Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True}, + "Hitboxes": {"type": "bool", "config_key": "hitboxes", "default": False}, }, "Credits": {} }