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

This commit is contained in:
csd4ni3l
2025-11-29 18:39:14 +01:00
parent 8b135e284a
commit df5cd8346d
5 changed files with 43 additions and 55 deletions

View File

@@ -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. 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.

View File

@@ -40,6 +40,14 @@ class Game(arcade.gui.UIView):
walls=[self.scene["walls"], self.scene["ice"]] 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.warmth = 50
self.direction = "right" self.direction = "right"
self.last_jump = time.perf_counter() self.last_jump = time.perf_counter()
@@ -50,6 +58,7 @@ class Game(arcade.gui.UIView):
self.restart_start = time.perf_counter() self.restart_start = time.perf_counter()
self.restarting = False self.restarting = False
self.won = False self.won = False
self.won_time = None
self.level_texts = [] self.level_texts = []
@@ -74,7 +83,7 @@ class Game(arcade.gui.UIView):
for level_num in range(AVAILABLE_LEVELS) 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) self.tries = self.data.get("tries", 1)
if self.best_time == 9999: if self.best_time == 9999:
self.no_besttime = True 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") 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): def reset(self, reached_end=False):
self.camera_shake.start()
if not reached_end: if not reached_end:
self.warmth = 50 self.warmth = 50
self.trees = 0 self.trees = 0
@@ -115,7 +126,7 @@ class Game(arcade.gui.UIView):
if self.no_besttime: if self.no_besttime:
self.no_besttime = False 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 = 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() self.back_button.on_click = lambda event: self.main_exit()
@@ -144,16 +155,23 @@ class Game(arcade.gui.UIView):
def on_draw(self): def on_draw(self):
self.clear() self.clear()
self.camera_shake.update_camera()
if not self.won: if not self.won:
with self.camera_sprites.activate(): with self.camera_sprites.activate():
self.scene.draw() 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: for level_text in self.level_texts:
level_text.draw() 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.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) 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() self.ui.draw()
def center_camera_to_player(self): def center_camera_to_player(self):
@@ -187,28 +205,33 @@ class Game(arcade.gui.UIView):
hit_list = self.physics_engine.update() hit_list = self.physics_engine.update()
self.center_camera_to_player() self.center_camera_to_player()
self.camera_shake.update(delta_time)
if self.player.collides_with_list(self.scene["end"]): 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: if self.no_besttime or end_time < self.best_time:
self.best_time = end_time self.best_time = end_time
self.won_time = end_time
self.reset(True) self.reset(True)
return return
if self.no_besttime: 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: if self.warmth <= 0 or self.player.collides_with_list(self.scene["spikes"]) or self.player.center_y < 0:
self.reset() self.reset()
if self.player.center_x + self.player.width / 2 < 0: if self.player.center_x + self.player.width / 2 < 0:
self.camera_shake.start()
self.player.center_x = self.player.width / 2 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: 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 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"]): 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: if checkpoint not in self.checkpoints_hit:
self.scene["checkpoints"].remove(checkpoint) self.scene["checkpoints"].remove(checkpoint)
self.checkpoints_hit.add(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 moved = False
ice_touch = any([ice_sprite in hit_list for ice_sprite in self.scene["ice"]]) and self.physics_engine.can_jump() 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]) 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): 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: 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: else:
self.player.change_x = 0 self.player.change_x = 0
@@ -260,6 +283,7 @@ class Game(arcade.gui.UIView):
self.warmth = self.clamp(self.warmth - 0.15, 0, 100) self.warmth = self.clamp(self.warmth - 0.15, 0, 100)
if self.warmth < 40: if self.warmth < 40:
self.camera_shake.start()
if self.settings.get("sfx", True) and not self.freeze_player.playing: if self.settings.get("sfx", True) and not self.freeze_player.playing:
self.freeze_player.play() self.freeze_player.play()
else: else:
@@ -268,13 +292,14 @@ class Game(arcade.gui.UIView):
if self.player.change_y > 0: if self.player.change_y > 0:
self.change_player_animation(player_jump_animation) 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) self.change_player_animation(player_walk_animation)
else: else:
self.change_player_animation(player_still_animation) self.change_player_animation(player_still_animation)
for level_text in self.level_texts: for level_text in self.level_texts:
if level_text.change_to_when_hit and self.player.rect.intersection(level_text.rect): 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 level_text.text = level_text.change_to_when_hit
self.player.update_animation() self.player.update_animation()

View File

@@ -147,22 +147,8 @@ class Settings(arcade.gui.UIView):
width, height = map(int, self.settings_dict['resolution'].split('x')) width, height = map(int, self.settings_dict['resolution'].split('x'))
self.window.set_size(width, height) self.window.set_size(width, height)
if self.settings_dict['vsync']: self.window.set_update_rate(1 / 60)
self.window.set_vsync(True) self.window.set_draw_rate(1 / 60)
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)
if self.settings_dict['discord_rpc']: if self.settings_dict['discord_rpc']:
if isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session. if isinstance(self.pypresence_client, FakePyPresence): # the user has enabled RPC in the settings in this session.

27
run.py
View File

@@ -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.utils import get_closest_resolution, print_debug_info, on_exception
from utils.constants import log_dir, menu_background_color from utils.constants import log_dir, menu_background_color
from menus.main import Main from menus.main import Main
# from utils.preload import theme_sound # needed for preload
from arcade.experimental.controller_window import ControllerWindow from arcade.experimental.controller_window import ControllerWindow
sys.excepthook = on_exception sys.excepthook = on_exception
# __builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args)))
if not log_dir in os.listdir(): if not log_dir in os.listdir():
os.makedirs(log_dir) os.makedirs(log_dir)
@@ -58,7 +55,6 @@ if os.path.exists('settings.json'):
fullscreen = settings['window_mode'] == 'Fullscreen' fullscreen = settings['window_mode'] == 'Fullscreen'
style = arcade.Window.WINDOW_STYLE_BORDERLESS if settings['window_mode'] == 'borderless' else arcade.Window.WINDOW_STYLE_DEFAULT style = arcade.Window.WINDOW_STYLE_BORDERLESS if settings['window_mode'] == 'borderless' else arcade.Window.WINDOW_STYLE_DEFAULT
vsync = settings['vsync'] vsync = settings['vsync']
fps_limit = settings['fps_limit']
else: else:
resolution = get_closest_resolution() resolution = get_closest_resolution()
antialiasing = 4 antialiasing = 4
@@ -73,7 +69,6 @@ else:
fullscreen = False fullscreen = False
style = arcade.Window.WINDOW_STYLE_DEFAULT style = arcade.Window.WINDOW_STYLE_DEFAULT
vsync = True vsync = True
fps_limit = 0
settings = { settings = {
"music": True, "music": True,
@@ -81,38 +76,20 @@ else:
"resolution": f"{resolution[0]}x{resolution[1]}", "resolution": f"{resolution[0]}x{resolution[1]}",
"antialiasing": "4x MSAA", "antialiasing": "4x MSAA",
"window_mode": "Windowed", "window_mode": "Windowed",
"vsync": True,
"fps_limit": 60,
"discord_rpc": True "discord_rpc": True
} }
with open("settings.json", "w") as file: with open("settings.json", "w") as file:
file.write(json.dumps(settings)) file.write(json.dumps(settings))
# if settings.get("music", True):
# theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True)
try: 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) 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: except (FileNotFoundError, PermissionError) as e:
logging.warning(f"Controller support unavailable: {e}. Falling back to regular window.") 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) 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_update_rate(1 / 60)
window.set_vsync(True) window.set_draw_rate(1 / 60)
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)
arcade.set_background_color(menu_background_color) arcade.set_background_color(menu_background_color)

View File

@@ -38,17 +38,14 @@ settings = {
"Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"}, "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"}, "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"}, "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": { "Sound": {
"Music": {"type": "bool", "config_key": "music", "default": True},
"SFX": {"type": "bool", "config_key": "sfx", "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}, "SFX Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "sfx_volume", "default": 50},
}, },
"Miscellaneous": { "Miscellaneous": {
"Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True}, "Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True},
"Hitboxes": {"type": "bool", "config_key": "hitboxes", "default": False},
}, },
"Credits": {} "Credits": {}
} }