Files
ember-keeper/game/play.py
2025-11-30 22:47:06 +01:00

517 lines
21 KiB
Python

import arcade, arcade.gui, json, time, os, random
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, menu_background_color, SNOWFLAKE_SPAWN_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"]],
platforms=[self.scene["x_moving_walls"], self.scene["y_moving_walls"]],
)
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 = 65
self.trees = 0
self.direction = "right"
self.last_jump = time.perf_counter()
self.start = time.perf_counter()
self.restart_start = time.perf_counter()
self.last_camera_shake = time.perf_counter()
self.last_snowflake_spawn = 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.paused = False
self.won_time = None
self.pause_time = 0
self.pause_start = time.perf_counter()
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 + 1}_best_time": 9999
for level_num in range(AVAILABLE_LEVELS)
}
self.data.update({
f"{level_num + 1}_tries": 0
for level_num in range(AVAILABLE_LEVELS)
})
self.data.update({
f"{level_num + 1}_best_replay": []
for level_num in range(AVAILABLE_LEVELS)
})
if self.settings.get("snowflakes", True):
self.snowflakes = arcade.SpriteList()
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.best_replay = self.data.get(f"{self.level_num}_best_replay", []).copy() if self.settings.get("replays", True) else []
self.replay_index = 0
if self.best_replay:
self.replay_player = arcade.TextureAnimationSprite(animation=player_still_animation, center_x=self.best_replay[0][0], center_y=self.best_replay[0][1], alpha=128)
self.replay_player.color = arcade.color.GRAY
self.scene.add_sprite(f"ReplayPlayer", self.replay_player)
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)
for falling_spike in self.scene["falling_spikes"]:
falling_spike.original_y = falling_spike.center_y
for x_moving_wall in self.scene["x_moving_walls"]:
x_moving_wall.x_movement = 0
x_moving_wall.x_direction = 1
for y_moving_wall in self.scene["y_moving_walls"]:
y_moving_wall.y_movement = 0
y_moving_wall.y_direction = -1
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, full=False):
if full:
self.disable_pause_menu()
self.warmth = 65
self.trees = 0
self.direction = "right"
self.last_jump = time.perf_counter()
self.start = time.perf_counter()
self.restart_start = time.perf_counter()
self.last_camera_shake = time.perf_counter()
self.checkpoints_hit = set()
self.collected_trees = []
self.last_replay_snapshot = time.perf_counter()
self.restarting = False
self.won = False
self.paused = False
self.won_time = None
self.spawn_position = tilemaps[self.level_num].object_lists["spawn"][0].shape
self.player.position = self.spawn_position
self.player.change_x, self.player.change_y = 0, 0
self.current_replay_data = []
self.replay_index = 0
return
self.shake_camera()
if not reached_end:
self.warmth = 65
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
self.update_data_file()
else:
if self.no_besttime:
self.no_besttime = False
self.anchor.remove(self.info_label)
arcade.set_background_color(menu_background_color)
self.anchor.add(arcade.gui.UILabel(text=f"Level Complete!\nTime: {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=65)
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
for falling_spike in self.scene["falling_spikes"]:
falling_spike.change_y = 0
falling_spike.center_y = falling_spike.original_y
if not self.checkpoints_hit:
self.start = time.perf_counter()
for level_text in self.level_texts:
level_text.text = level_text.original_text
for tree in self.collected_trees:
self.scene["trees"].append(tree)
self.collected_trees = []
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()
if not self.won:
if self.settings.get("snowflakes", True):
self.snowflakes.draw()
self.camera_shake.update_camera()
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()
if self.settings.get("replays", True):
for i in range(len(self.current_replay_data[-20:]) - 1):
trail_pos = self.current_replay_data[-20:][i]
next_pos = self.current_replay_data[-20:][i + 1]
arcade.draw_line(trail_pos[0], trail_pos[1], next_pos[0], next_pos[1], arcade.color.RED, 3)
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 shake_camera(self):
if time.perf_counter() - self.last_camera_shake >= 0.25:
self.camera_shake.start()
self.last_camera_shake = time.perf_counter()
def on_update(self, delta_time: float):
if self.won or self.paused:
return
if self.settings.get("snowflakes", True):
if time.perf_counter() - self.last_snowflake_spawn >= SNOWFLAKE_SPAWN_DELAY:
snowflake = arcade.SpriteCircle(5, arcade.color.LIGHT_BLUE)
snowflake.center_x = random.randint(5, self.window.width - 5)
snowflake.center_y = self.window.height - 10
snowflake.y_direction = -random.uniform(30, 60)
snowflake.x_direction = random.uniform(-15, 15)
self.snowflakes.append(snowflake)
self.last_snowflake_spawn = time.perf_counter()
for snowflake in self.snowflakes:
snowflake.center_y += snowflake.y_direction * delta_time
snowflake.center_x += snowflake.x_direction * delta_time
if snowflake.center_y < -10:
self.snowflakes.remove(snowflake)
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 - self.pause_time, 4)
if self.no_besttime or end_time <= self.best_time:
self.best_time = end_time
self.update_data_file(with_replay=True)
self.won_time = end_time
self.reset(True)
return
if self.no_besttime:
self.best_time = round(time.perf_counter() - self.start - self.pause_time, 4)
self.info_label.text = f"Time took: {round(time.perf_counter() - self.start - self.pause_time, 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.shake_camera()
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.shake_camera()
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 + 20, 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)
if self.restarting: # restart here so player doesn't move, but physics engine still updates
if time.perf_counter() - self.restart_start >= RESTART_DELAY:
self.restarting = False
else:
return
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.shake_camera()
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.warmth > 50:
arcade.set_background_color(menu_background_color)
elif self.warmth > 35:
arcade.set_background_color(arcade.color.DARK_BLUE)
elif self.warmth > 15:
arcade.set_background_color(arcade.color.BLUE)
else:
arcade.set_background_color(arcade.color.LIGHT_BLUE)
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.shake_camera()
level_text.text = level_text.change_to_when_hit
self.player.update_animation()
for x_moving_wall in self.scene["x_moving_walls"]:
x_moving_wall.center_x += 3 * x_moving_wall.x_direction
x_moving_wall.x_movement += 3
if x_moving_wall.x_movement > 140:
x_moving_wall.x_movement = 0
x_moving_wall.x_direction *= -1
for y_moving_wall in self.scene["y_moving_walls"]:
y_moving_wall.center_y += 3 * y_moving_wall.y_direction
y_moving_wall.y_movement += 3
if y_moving_wall.y_movement > 140:
y_moving_wall.y_movement = 0
y_moving_wall.y_direction *= -1
if self.scene._name_mapping.get("falling_spikes"):
for falling_spike in self.scene["falling_spikes"]:
if abs(self.player.rect.distance_from_bounds(falling_spike.position)) < 100:
falling_spike.change_y = -GRAVITY * 3
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.best_replay:
self.replay_index += 1
if self.replay_index < len(self.best_replay) - 1:
self.replay_player.center_x, self.replay_player.center_y = self.best_replay[self.replay_index]
else:
self.best_replay = None
self.replay_player = None
if f"ReplayPlayer" in self.scene._name_mapping:
self.scene.remove_sprite_list_by_name(f"ReplayPlayer")
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 and self.current_replay_data:
data_dict[f"{self.level_num}_best_replay"] = self.current_replay_data
file.write(json.dumps(data_dict, indent=4))
def pause_menu(self):
self.pause_start = time.perf_counter()
self.paused = True
self.pause_box = self.anchor.add(arcade.gui.UIBoxLayout(align="center", size_hint=(0.3, 0.5), space_between=10).with_background(color=arcade.color.DARK_GRAY), anchor_x="center", anchor_y="center")
self.pause_box.add(arcade.gui.UISpace(height=self.window.height * 0.025))
self.pause_box.add(arcade.gui.UILabel("Paused", font_size=28, text_color=arcade.color.BLACK))
self.pause_box.add(arcade.gui.UISpace(height=self.window.height * 0.025))
resume_button = self.pause_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Resume', style=button_style, width=self.window.width * 0.25, height=self.window.height * 0.1))
resume_button.on_click = lambda event: self.disable_pause_menu()
restart_button = self.pause_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Restart', style=button_style, width=self.window.width * 0.25, height=self.window.height * 0.1))
restart_button.on_click = lambda event: self.reset(full=True)
exit_button = self.pause_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Exit', style=button_style, width=self.window.width * 0.25, height=self.window.height * 0.1))
exit_button.on_click = lambda event: self.main_exit()
def disable_pause_menu(self):
self.pause_time += time.perf_counter() - self.pause_start
self.paused = False
self.anchor.remove(self.pause_box)
def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE:
self.pause_menu()
def main_exit(self):
arcade.set_background_color(menu_background_color)
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))