import arcade, arcade.gui, json, time, os from utils.constants import FOLLOW_DECAY_CONST, GRAVITY, PLAYER_MOVEMENT_SPEED, PLAYER_JUMP_SPEED, GRID_PIXEL_SIZE, PLAYER_JUMP_COOLDOWN, LEFT_RIGHT_DIAGONAL_ID, RIGHT_LEFT_DIAGONAL_ID, AVAILABLE_LEVELS, RESTART_DELAY, button_style, REPLAY_DELAY from utils.preload import tilemaps, player_still_animation, player_jump_animation, player_walk_animation, freeze_sound, background_sound, button_texture, button_hovered_texture class Game(arcade.gui.UIView): def __init__(self, pypresence_client, level_num): super().__init__() self.pypresence_client = pypresence_client self.pypresence_client.update(state="Keeping the warmth") self.camera_sprites = arcade.Camera2D() self.camera_bounds = self.window.rect self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.level_num = level_num self.scene = self.create_scene() self.right_left_diagonal_sprites = [ sprite for sprite in self.scene["ice"] if hasattr(sprite, 'properties') and sprite.properties.get('tile_id') == RIGHT_LEFT_DIAGONAL_ID ] self.left_right_diagonal_sprites = [ sprite for sprite in self.scene["ice"] if hasattr(sprite, 'properties') and sprite.properties.get('tile_id') == LEFT_RIGHT_DIAGONAL_ID ] self.spawn_position = tilemaps[self.level_num].object_lists["spawn"][0].shape player_x, player_y = self.spawn_position self.player = arcade.TextureAnimationSprite(animation=player_still_animation, center_x=player_x, center_y=player_y) self.physics_engine = arcade.PhysicsEnginePlatformer( self.player, gravity_constant=GRAVITY, 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.trees = 0 self.direction = "right" self.last_jump = time.perf_counter() self.start = time.perf_counter() self.restart_start = time.perf_counter() self.checkpoints_hit = set() self.collected_trees = [] self.current_replay_data = [] self.last_replay_snapshot = time.perf_counter() self.restarting = False self.won = False self.won_time = None self.level_texts = [] for tile in tilemaps[self.level_num].object_lists["text"]: self.level_texts.append(arcade.Text(tile.name, tile.shape[0], tile.shape[1], font_size=14)) self.level_texts[-1].original_text = tile.name self.level_texts[-1].change_to_when_hit = tile.properties.get("change_to_when_hit") with open("settings.json", "r") as file: self.settings = json.load(file) if os.path.exists("data.json"): with open("data.json", "r") as file: self.data = json.load(file) else: self.data = { f"{level_num}_best_time": 9999 for level_num in range(AVAILABLE_LEVELS) } self.data.update({ f"{level_num}_tries": 0 for level_num in range(AVAILABLE_LEVELS) }) self.data["replays"] = [] 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 self.best_time = 0 else: self.no_besttime = False self.scene.add_sprite("Player", self.player) self.replays = self.data.get("replays", []).copy() if self.settings.get("replays", True) else [] self.replay_players = [] self.replay_indices = [0] * len(self.replays) if self.replays: for n, replay in enumerate(self.replays): self.replay_players.append(arcade.TextureAnimationSprite(animation=player_still_animation, center_x=replay[0][0], center_y=replay[0][1], alpha=128)) self.replay_players[-1].color = arcade.color.GRAY self.scene.add_sprite(f"ReplayPlayer{n}", self.replay_players[-1]) if self.settings.get("sfx", True): self.freeze_player = freeze_sound.play(loop=True, volume=self.settings.get("sfx_volume", 100) / 100) self.freeze_player.pause() self.background_player = background_sound.play(loop=True, volume=self.settings.get("sfx_volume", 100) / 100) def on_show_view(self): super().on_show_view() 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 self.player.change_x, self.player.change_y = 0, 0 self.player.position = self.spawn_position self.tries += 1 self.restart_start = time.perf_counter() self.restarting = True if self.no_besttime: self.best_time = 9999 else: if self.no_besttime: self.no_besttime = False 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() self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=10, align_y=-10) self.won = True if not self.checkpoints_hit: self.start = time.perf_counter() for level_text in self.level_texts: level_text.text = level_text.original_text self.update_data_file() def create_scene(self) -> arcade.Scene: self.camera_bounds = arcade.LRBT( self.window.width/2.0, tilemaps[self.level_num].width * GRID_PIXEL_SIZE - self.window.width/2.0, self.window.height/2.0, tilemaps[self.level_num].height * GRID_PIXEL_SIZE ) return arcade.Scene.from_tilemap(tilemaps[self.level_num]) 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): self.camera_sprites.position = arcade.math.smerp_2d( self.camera_sprites.position, self.player.position, self.window.delta_time, FOLLOW_DECAY_CONST, ) self.camera_sprites.view_data.position = arcade.camera.grips.constrain_xy( self.camera_sprites.view_data, self.camera_bounds ) def clamp(self, value, min_value, max_value): return max(min_value, min(value, max_value)) def change_player_animation(self, animation): if self.player.animation != animation: self.player.animation = animation def on_update(self, delta_time: float): if self.won: return if self.restarting: if time.perf_counter() - self.restart_start >= RESTART_DELAY: self.restarting = False else: return 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, 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, 4) 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"]): self.trees += 1 self.collected_trees.append(tree) self.scene["trees"].remove(tree) self.warmth = self.clamp(self.warmth + 35, 0, 100) for checkpoint in self.player.collides_with_list(self.scene["checkpoints"]): 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 / 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() if self.window.keyboard[arcade.key.UP] or self.window.keyboard[arcade.key.SPACE]: if time.perf_counter() - self.last_jump >= PLAYER_JUMP_COOLDOWN and self.physics_engine.can_jump(): self.last_jump = time.perf_counter() self.player.change_y = PLAYER_JUMP_SPEED if self.window.keyboard[arcade.key.LEFT] or self.window.keyboard[arcade.key.A]: moved = True self.player.change_x = -PLAYER_MOVEMENT_SPEED self.direction = "left" elif self.window.keyboard[arcade.key.RIGHT] or self.window.keyboard[arcade.key.D]: moved = True self.player.change_x = PLAYER_MOVEMENT_SPEED self.direction = "right" else: if ice_touch: on_left_right_diagonal = any([True for hit in hit_list if hit in self.left_right_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): 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.3) else: self.player.change_x = 0 if moved and ice_touch: self.player.change_x *= 1.5 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: if self.settings.get("sfx", True): self.freeze_player.pause() if self.player.change_y > 0: self.change_player_animation(player_jump_animation) 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() if time.perf_counter() - self.last_replay_snapshot >= REPLAY_DELAY: self.last_replay_snapshot = time.perf_counter() self.current_replay_data.append([self.player.center_x, self.player.center_y]) if self.replays: replays_to_remove = [] for n, replay in enumerate(self.replays): if replay is None: continue self.replay_indices[n] += 1 if self.replay_indices[n] < len(replay): self.replay_players[n].center_x, self.replay_players[n].center_y = replay[self.replay_indices[n]] else: replays_to_remove.append(n) for replay_to_remove in replays_to_remove: self.replays[replay_to_remove] = None self.replay_players[replay_to_remove] = None if f"ReplayPlayer{replay_to_remove}" in self.scene._name_mapping: self.scene.remove_sprite_list_by_name(f"ReplayPlayer{replay_to_remove}") def update_data_file(self, with_replay=False): with open("data.json", "w") as file: data_dict = self.data.copy() data_dict.update({ f"{self.level_num}_best_time": self.best_time, f"{self.level_num}_tries": self.tries }) if with_replay: if self.current_replay_data: data_dict["replays"].append(self.current_replay_data) file.write(json.dumps(data_dict, indent=4)) def on_key_press(self, symbol, modifiers): if symbol == arcade.key.ESCAPE: self.main_exit() def main_exit(self): self.update_data_file(with_replay=True) from menus.main import Main self.window.show_view(Main(self.pypresence_client))