diff --git a/package-lock.json b/package-lock.json index 6a434b0..2fb7383 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "smb-web", "dependencies": { + "fflate": "^0.8.2", "gl-matrix": "^3.4.3" }, "devDependencies": { @@ -497,6 +498,12 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", diff --git a/package.json b/package.json index 7cd9657..8531dfa 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "smb-web", "private": true, "dependencies": { + "fflate": "^0.8.2", "gl-matrix": "^3.4.3" }, "devDependencies": { diff --git a/tools/done.txt b/tools/done.txt new file mode 100644 index 0000000..154c61e --- /dev/null +++ b/tools/done.txt @@ -0,0 +1 @@ +twitter 2016367040261435392 diff --git a/tools/smb2_pack_builder.py b/tools/smb2_pack_builder.py index 343bf30..2122635 100644 --- a/tools/smb2_pack_builder.py +++ b/tools/smb2_pack_builder.py @@ -503,6 +503,7 @@ def build_pack( zip_output: bool, courses_data: Optional[Dict[str, object]] = None, lst_path: Optional[Path] = None, + stage_time_overrides: Optional[Dict[int, int]] = None, ) -> None: main_loop_rel = rom_dir / 'mkb2.main_loop.rel' stgname = rom_dir / 'stgname' / 'usa.str' @@ -623,15 +624,19 @@ def build_pack( if not referenced_bgs: warnings.append('no backgrounds referenced from stage env data') + content = { + 'stages': stage_ids, + 'stageNames': {str(k): v for k, v in stage_names.items()}, + } + if stage_time_overrides: + content['stageTimeOverrides'] = {str(k): v for k, v in stage_time_overrides.items()} + pack_manifest = { 'id': pack_id, 'name': pack_name, 'gameSource': 'smb2', 'version': 1, - 'content': { - 'stages': stage_ids, - 'stageNames': {str(k): v for k, v in stage_names.items()}, - }, + 'content': content, 'courses': courses, 'stageEnv': stage_env, } @@ -760,6 +765,7 @@ def run_gui() -> None: story_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) challenge_courses: Dict[str, List[Tuple[int, bool]]] = {} + stage_time_overrides: Dict[int, int] = {} selected_course_name = tk.StringVar() course_list = tk.Listbox(challenge_frame, height=8) @@ -822,7 +828,9 @@ def run_gui() -> None: if not name: return for stage_id, bonus in challenge_courses.get(name, []): - label = f'{stage_id} {"(bonus)" if bonus else ""}' + time_override = stage_time_overrides.get(stage_id) + time_label = f' ({time_override // 60}s)' if time_override else '' + label = f'{stage_id}{time_label} {"(bonus)" if bonus else ""}' stage_list.insert(tk.END, label.strip()) stage_controls = ttk.Frame(challenge_frame) @@ -830,8 +838,13 @@ def run_gui() -> None: stage_id_var = tk.StringVar() stage_bonus_var = tk.BooleanVar(value=False) - ttk.Entry(stage_controls, textvariable=stage_id_var, width=8).pack(side=tk.LEFT, padx=(0, 6)) + stage_time_var = tk.StringVar() + stage_id_entry = ttk.Entry(stage_controls, textvariable=stage_id_var, width=8) + stage_id_entry.pack(side=tk.LEFT, padx=(0, 6)) ttk.Checkbutton(stage_controls, text='Bonus', variable=stage_bonus_var).pack(side=tk.LEFT, padx=(0, 6)) + stage_time_entry = ttk.Entry(stage_controls, textvariable=stage_time_var, width=6) + stage_time_entry.pack(side=tk.LEFT, padx=(0, 6)) + ttk.Label(stage_controls, text='sec').pack(side=tk.LEFT, padx=(0, 6)) def add_stage(): name = selected_course_name.get() @@ -842,12 +855,29 @@ def run_gui() -> None: if not raw.isdigit(): messagebox.showerror('Invalid stage', 'Stage ID must be a number.') return + raw_time = stage_time_var.get().strip() + if raw_time: + if not raw_time.isdigit(): + messagebox.showerror('Invalid time', 'Time must be a number of seconds.') + return + stage_time_overrides[int(raw)] = int(raw_time) * 60 stage_id = int(raw) bonus = bool(stage_bonus_var.get()) challenge_courses[name].append((stage_id, bonus)) stage_id_var.set('') + stage_time_var.set('') stage_bonus_var.set(False) refresh_stage_list() + stage_id_entry.focus_set() + + def stage_in_use(stage_id: int) -> bool: + for entries in challenge_courses.values(): + if any(stage_id == sid for sid, _ in entries): + return True + for world in story_worlds: + if stage_id in world: + return True + return False def remove_stage(): name = selected_course_name.get() @@ -857,7 +887,9 @@ def run_gui() -> None: idx = selection[0] items = challenge_courses.get(name, []) if 0 <= idx < len(items): - items.pop(idx) + stage_id, _ = items.pop(idx) + if not stage_in_use(stage_id): + stage_time_overrides.pop(stage_id, None) refresh_stage_list() def toggle_bonus(): @@ -875,6 +907,8 @@ def run_gui() -> None: ttk.Button(stage_controls, text='Add stage', command=add_stage).pack(side=tk.LEFT, padx=(0, 6)) ttk.Button(stage_controls, text='Remove', command=remove_stage).pack(side=tk.LEFT, padx=(0, 6)) ttk.Button(stage_controls, text='Toggle bonus', command=toggle_bonus).pack(side=tk.LEFT) + stage_id_entry.bind('', lambda _event: add_stage()) + stage_time_entry.bind('', lambda _event: add_stage()) story_worlds: List[List[int]] = [] world_list = tk.Listbox(story_frame, height=8) @@ -926,7 +960,12 @@ def run_gui() -> None: world_stage_controls.pack(fill=tk.X) world_stage_id_var = tk.StringVar() - ttk.Entry(world_stage_controls, textvariable=world_stage_id_var, width=8).pack(side=tk.LEFT, padx=(0, 6)) + world_stage_time_var = tk.StringVar() + world_stage_entry = ttk.Entry(world_stage_controls, textvariable=world_stage_id_var, width=8) + world_stage_entry.pack(side=tk.LEFT, padx=(0, 6)) + world_stage_time_entry = ttk.Entry(world_stage_controls, textvariable=world_stage_time_var, width=6) + world_stage_time_entry.pack(side=tk.LEFT, padx=(0, 6)) + ttk.Label(world_stage_controls, text='sec').pack(side=tk.LEFT, padx=(0, 6)) def add_world_stage(): selection = world_list.curselection() @@ -937,10 +976,18 @@ def run_gui() -> None: if not raw.isdigit(): messagebox.showerror('Invalid stage', 'Stage ID must be a number.') return + raw_time = world_stage_time_var.get().strip() + if raw_time: + if not raw_time.isdigit(): + messagebox.showerror('Invalid time', 'Time must be a number of seconds.') + return + stage_time_overrides[int(raw)] = int(raw_time) * 60 idx = selection[0] story_worlds[idx].append(int(raw)) world_stage_id_var.set('') + world_stage_time_var.set('') refresh_world_stage_list() + world_stage_entry.focus_set() def remove_world_stage(): selection = world_list.curselection() @@ -950,11 +997,15 @@ def run_gui() -> None: world_idx = selection[0] stage_idx = stage_selection[0] if 0 <= stage_idx < len(story_worlds[world_idx]): - story_worlds[world_idx].pop(stage_idx) + stage_id = story_worlds[world_idx].pop(stage_idx) + if not stage_in_use(stage_id): + stage_time_overrides.pop(stage_id, None) refresh_world_stage_list() ttk.Button(world_stage_controls, text='Add stage', command=add_world_stage).pack(side=tk.LEFT, padx=(0, 6)) ttk.Button(world_stage_controls, text='Remove', command=remove_world_stage).pack(side=tk.LEFT) + world_stage_entry.bind('', lambda _event: add_world_stage()) + world_stage_time_entry.bind('', lambda _event: add_world_stage()) def build_courses_data() -> Dict[str, object]: order = {} @@ -1000,6 +1051,7 @@ def run_gui() -> None: bool(zip_var.get()), courses_data=courses_data, lst_path=Path(lst_var.get().strip()) if lst_var.get().strip() else None, + stage_time_overrides=stage_time_overrides, ) except Exception as exc: messagebox.showerror('Build failed', str(exc))