Add DAT finale flow, editor, and new soundtrack assets

This commit is contained in:
2026-05-09 12:03:17 +02:00
parent f0d056e7f0
commit 310dc0dca9
22 changed files with 1982 additions and 106 deletions
+105 -21
View File
@@ -15,6 +15,7 @@ from runtime_paths import bundle_path
LEVEL_MUSIC_CONFIG = "level_music"
START_MENU_MUSIC = "High_Score_Garden.mp3"
RUN_COMPLETE_MUSIC = "Sunset_At_Pixel_Gardens.mp3"
START_MENU_ANIMATION = "anim/start_mice.gif"
SUPPORTED_MUSIC_EXTENSIONS = {".mp3", ".ogg", ".wav"}
BASE_INITIAL_RATS = 5
@@ -28,6 +29,9 @@ START_MENU_AUDIO_OPTIONS = (
("music_volume", "Musica"),
)
VOLUME_STEP = 5
GAME_END_LEVEL_CLEAR = "level_clear"
GAME_END_DEFEAT = "defeat"
GAME_END_RUN_COMPLETE = "run_complete"
DIFFICULTY_OPTIONS = (
{
"key": "easy",
@@ -90,9 +94,9 @@ class MiceMaze(
# Initialize user profile integration
self.profile_integration = UserProfileIntegration()
self.map_source = maze_file
self.current_level = level_index
self.map = maze.Map(maze_file, level_index=level_index)
self.current_level = self.map.level_index
self.total_levels = self.map.level_count
# Load profile-specific settings
self.audio = self.profile_integration.get_setting('sound_enabled', True)
@@ -242,6 +246,38 @@ class MiceMaze(
next_index = (current_index + delta) % len(DIFFICULTY_OPTIONS)
self._apply_difficulty(DIFFICULTY_OPTIONS[next_index]["key"])
def _is_dat_campaign(self):
return self.map.source_path.suffix.lower() == ".dat"
def _is_last_dat_level(self):
return self._is_dat_campaign() and self.current_level >= self.total_levels - 1
def _normalize_target_level(self, level_index):
if self._is_dat_campaign():
return maze.normalize_level_index(level_index, self.total_levels)
return 0
def _record_run_result(self, completed):
if not self.run_recorded:
self.save_score()
self.profile_integration.update_game_stats(self.points, completed=completed)
self.run_recorded = True
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
def activate_debug_run_complete_dialog(self, score=None):
if score is not None:
self.points = max(0, int(score))
self.current_level = max(0, self.total_levels - 1)
self.game_status = "paused"
self.menu_screen = None
self.game_end = (True, GAME_END_RUN_COMPLETE)
self.run_recorded = True
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
print(
f"[debug] showing run-complete dialog: level={self.current_level + 1}/{self.total_levels} "
f"points={self.points}"
)
def start_game(self):
print(
@@ -292,15 +328,17 @@ class MiceMaze(
self.spawn_rat()
def load_level(self, level_index, preserve_points=True, show_menu=False, menu_screen=None):
next_theme_index = level_index % maze.LEVELS_PER_DAT_FILE // 8 + 1
target_level_index = self._normalize_target_level(level_index)
next_theme_index = target_level_index // 8 + 1
print(
f"[flow] load_level requested: target_level={level_index + 1} "
f"[flow] load_level requested: target_level={target_level_index + 1} "
f"preserve_points={preserve_points} show_menu={show_menu} menu_screen={menu_screen} "
f"current_points={self.points} next_theme={next_theme_index}"
)
self.current_level = level_index
self.current_level_music = self._resolve_level_music(level_index)
self.map = maze.Map(self.map_source, level_index=level_index)
self.map = maze.Map(self.map_source, level_index=target_level_index)
self.current_level = self.map.level_index
self.total_levels = self.map.level_count
self.current_level_music = self._resolve_level_music(self.current_level)
self._valid_positions = None
self.collision_system = CollisionSystem(
self.cell_size,
@@ -357,11 +395,18 @@ class MiceMaze(
def advance_level(self):
print(f"[flow] advance_level called from level={self.current_level + 1} points={self.points}")
if self.map.source_path.suffix.lower() != ".dat":
if not self._is_dat_campaign():
self.load_level(self.current_level, preserve_points=True, show_menu=True, menu_screen="level_intro")
return
next_level = (self.current_level + 1) % maze.LEVELS_PER_DAT_FILE
if self._is_last_dat_level():
print("[flow] advance_level -> reached end of DAT campaign")
self.game_end = (True, GAME_END_RUN_COMPLETE)
self.game_status = "paused"
self._record_run_result(completed=True)
return
next_level = self.current_level + 1
print(f"[flow] advancing to level={next_level + 1}")
self.load_level(next_level, preserve_points=True, show_menu=True, menu_screen="level_intro")
@@ -371,9 +416,13 @@ class MiceMaze(
f"menu_screen={self.menu_screen} points={self.points} level={self.current_level + 1}"
)
if self.game_end[0]:
if self.game_end[1]:
if self.game_end[1] == GAME_END_LEVEL_CLEAR:
print("[flow] reset_game -> post-victory path")
self.advance_level()
elif self.game_end[1] == GAME_END_RUN_COMPLETE:
print("[flow] reset_game -> DAT run complete, returning to start menu")
self.start_menu_animation_started_at = time.monotonic()
self.load_level(0, preserve_points=False, show_menu=True, menu_screen="start")
else:
print("[flow] reset_game -> restart from level 1 after defeat")
self.load_level(0, preserve_points=False, show_menu=False)
@@ -766,6 +815,9 @@ class MiceMaze(
self._adjust_selected_volume(VOLUME_STEP)
def update_background_music(self):
if self.game_end[0] and self.game_end[1] == GAME_END_RUN_COMPLETE:
self.render_engine.play_music(RUN_COMPLETE_MUSIC, loop=True)
return
if self.game_status == "game" and not self.game_end[0]:
if self.current_level_music:
self.render_engine.play_music(self.current_level_music, loop=True)
@@ -874,14 +926,21 @@ class MiceMaze(
if self.game_end[0]:
if self.combined_scores is None:
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
if not self.game_end[1]:
if self.game_end[1] == GAME_END_DEFEAT:
self.render_engine.dialog(
"Game Over: Mice are too many!",
image=self.assets["BMP_WEWIN"],
subtitle=f"Reached level: {self.current_level + 1}\nPress Return to restart from level 1",
scores=self.combined_scores
)
elif self.game_end[1] == GAME_END_RUN_COMPLETE:
self.render_engine.dialog(
"THE END",
image=self.assets.get("end", self.assets["BMP_WEWIN"]),
current_score=self.points,
style="run_complete",
)
else:
self.render_engine.dialog(
f"Level {self.current_level + 1} Clear! Points: {self.points}",
@@ -896,24 +955,27 @@ class MiceMaze(
if count_rats > 200:
self.render_engine.stop_sound()
self.render_engine.play_sound("WEWIN.WAV")
self.game_end = (True, False)
self.game_end = (True, GAME_END_DEFEAT)
self.game_status = "paused"
print(f"[flow] defeat reached: rats={count_rats} points={self.points} level={self.current_level + 1}")
if not self.run_recorded:
self.save_score()
self.profile_integration.update_game_stats(self.points, completed=False)
self.run_recorded = True
self._record_run_result(completed=False)
return True
if not count_rats and not any(isinstance(unit, points.Point) for unit in self.units.values()):
self.render_engine.stop_sound()
self.render_engine.play_sound("VICTORY.WAV")
self.render_engine.play_sound("WELLDONE.WAV", tag="effects")
self.game_end = (True, True)
self.game_status = "paused"
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
print(f"[flow] victory reached: points={self.points} level={self.current_level + 1}")
if self._is_last_dat_level():
self.game_end = (True, GAME_END_RUN_COMPLETE)
self._record_run_result(completed=True)
print(f"[flow] final DAT victory reached: points={self.points} level={self.current_level + 1}")
else:
self.game_end = (True, GAME_END_LEVEL_CLEAR)
self.combined_scores = self.profile_integration.get_device_leaderboard(5)
print(f"[flow] victory reached: points={self.points} level={self.current_level + 1}")
return True
@@ -921,9 +983,29 @@ class MiceMaze(
def parse_args():
def parse_debug_score(value):
try:
parsed = float(str(value).strip())
except ValueError as exc:
raise argparse.ArgumentTypeError(f"invalid int value: {value!r}") from exc
if not parsed.is_integer():
raise argparse.ArgumentTypeError(f"invalid int value: {value!r}")
return int(parsed)
parser = argparse.ArgumentParser(description="Run Mice! with DAT or JSON map loading")
parser.add_argument("--level", type=int, default=0, help="Level index to load from level.dat (default: 0)")
parser.add_argument("--map", dest="map_path", default=None, help="Optional map path override (.dat or .json)")
parser.add_argument(
"--debug-run-complete-dialog",
action="store_true",
help="Start directly on the final run-complete dialog without saving a fake high score",
)
parser.add_argument(
"--debug-final-score",
type=parse_debug_score,
default=1125,
help="Score shown by --debug-run-complete-dialog (default: 1125)",
)
return parser.parse_args()
@@ -933,5 +1015,7 @@ if __name__ == "__main__":
map_source = args.map_path or maze.get_default_map_source()
print(f"Loading map from {map_source} (level {args.level})")
solver = MiceMaze(map_source, level_index=args.level)
if args.debug_run_complete_dialog:
solver.activate_debug_run_complete_dialog(score=args.debug_final_score)
solver.run()