Commit
the-way-out
v0.2.7
modified CHANGELOG.md
@@ -1,5 +1,25 @@
# CHANGELOG
## v0.2.7
A playable title screen, in the style of an Assassin's Creed loading
screen. No save-file format changes; existing saves load as-is.
### Title screen
- The main menu is now playable: your selected character spawns on
the title screen and can be moved (WASD/Arrows), dashed (Shift) and
fired (Space) while the menu buttons stay clickable. The camera does
not move — the avatar is confined to the window.
- The wandering background figures are purely decorative ghosts: the
player walks through them, projectiles pass through them, and they
never attack or react.
- Left mouse no longer fires on the title screen so clicks only
operate the menu buttons; in-game, left mouse still shoots as
before.
- Picking a different character in the Characters menu immediately
updates the avatar shown on the title screen.
## v0.2.6
A small polish release: two missing-glyph fixes in the UI and a
modified VERSION
@@ -1 +1 @@
v0.2.6
v0.2.7
modified main.py
@@ -293,6 +293,7 @@ while running:
action = character_menu.handle_input(event)
if action:
current_character = action
main_menu.set_character(current_character)
game_state = "menu"
# Levels select — action is the chosen level id (from catalog).
@@ -341,10 +342,12 @@ while running:
# Draw & Update --------------------------------------------------
if game_state == "menu":
main_menu.update(dt)
main_menu.draw(screen)
elif game_state == "updating":
update_anim_t += dt
result = update_state["result"]
main_menu.update(dt)
if result == "done":
main_menu.set_status("Updated - restarting...", ttl=None)
main_menu.draw(screen)
modified menu.py
@@ -60,6 +60,78 @@ class MainMenu:
# vignette. Replaces the prior PixelDust on the title screen.
self.scene = theme.MenuScene(width, height, seed=7)
# Playable avatar overlay (AC-style loading screen). The
# wandering MenuScene actors stay non-interactive — they have no
# hitbox and aren't in any group, so the player walks through
# them and shots can't hit them. Bounds are enforced by a
# screen-rect clamp in update(); walls/targets are empty groups.
self._character_classes = {
key: cls for key, cls, _label, _tag in CHARACTER_INFO}
self.world_obstacles = pygame.sprite.Group()
self.projectile_targets = pygame.sprite.Group()
self.projectile_group = pygame.sprite.Group()
self.player = None
self._spawn_player("c_wiz")
def _spawn_player(self, key):
cls = self._character_classes.get(key)
if cls is None:
return
# Bottom-center, well clear of the title and buttons. The clamp
# in update() keeps the player on screen no matter the spawn.
spawn_x = self.width // 2
spawn_y = self.height - 220
self.player = cls(spawn_x, spawn_y, self.world_obstacles)
# Center the sprite on the requested spawn point.
self.player.rect.center = (spawn_x, spawn_y)
self.player.pos.update(self.player.rect.topleft)
self.player.hitbox.center = self.player.rect.center
self.player.projectile_group = self.projectile_group
self.player.projectile_targets = self.projectile_targets
# Left mouse must stay reserved for clicking menu buttons.
self.player.attack_mouse_enabled = False
self.current_character_key = key
def set_character(self, key):
"""Rebuild the menu avatar when CharacterMenu picks a new one."""
if key == getattr(self, "current_character_key", None):
return
for shot in list(self.projectile_group):
shot.kill()
self._spawn_player(key)
def update(self, dt):
if self.player is None:
return
self.player.update(dt)
self.projectile_group.update(dt)
# Clamp the player to the screen rect. The level's wall-collide
# path is unavailable here (no walls), so cap pos/rect/hitbox
# together to keep the sprite, draw rect and shot-spawn point in
# sync.
rect = self.player.rect
max_x = self.width - rect.width
max_y = self.height - rect.height
if self.player.pos.x < 0:
self.player.pos.x = 0
elif self.player.pos.x > max_x:
self.player.pos.x = max_x
if self.player.pos.y < 0:
self.player.pos.y = 0
elif self.player.pos.y > max_y:
self.player.pos.y = max_y
rect.topleft = (round(self.player.pos.x), round(self.player.pos.y))
self.player.hitbox.center = rect.center
# Prune shots that left the screen; Projectile.update would
# eventually drop them via PROJECTILE_LIFETIME, but clearing
# off-screen orbs early keeps the group tight.
screen_rect = pygame.Rect(0, 0, self.width, self.height)
for shot in list(self.projectile_group):
if not screen_rect.colliderect(shot.rect):
shot.kill()
def set_status(self, text, ttl=4.0):
"""Set the toast text. ``ttl`` is seconds until main.py clears
it; pass ``None`` for a status that should persist (animated
@@ -80,6 +152,14 @@ class MainMenu:
# work (submenus keep theirs — PixelDust is sparse, does not
# cover the screen).
self.scene.draw(screen)
# AC-style loading-screen overlay: shots under the player, both
# above the scene and below the title/buttons so the UI stays
# readable and clickable.
self.projectile_group.draw(screen)
if self.player is not None:
screen.blit(self.player.image, self.player.rect)
mouse_pos = pygame.mouse.get_pos()
title = theme.text_surface(self.title_font, "THE WAY OUT", TITLE_C)
modified units.py
@@ -46,6 +46,11 @@ class Character(pygame.sprite.Sprite):
attack_damage = PROJECTILE_DAMAGE
attack_cooldown = ATTACK_COOLDOWN
# When False, left mouse no longer triggers an attack — only Space
# fires. The main-menu's playable avatar sets this off so clicks on
# buttons don't double as shots; gameplay keeps the default.
attack_mouse_enabled = True
# name -> frame count in the sprite sheet
SPRITE_SHEETS = {
'idle_down': ('D_Idle', 4),
@@ -259,7 +264,8 @@ class Character(pygame.sprite.Sprite):
self.attack_timer = max(0.0, self.attack_timer - dt)
keys = pygame.key.get_pressed()
mouse_left = pygame.mouse.get_pressed()[0]
mouse_left = (self.attack_mouse_enabled
and pygame.mouse.get_pressed()[0])
if (keys[pygame.K_SPACE] or mouse_left) and self.attack_timer <= 0:
aim = pygame.math.Vector2(*FACING_VECTORS[self.facing])
spawn = pygame.math.Vector2(self.hitbox.center)