-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary: Beyond GameScene.js, there are a few other oversized or multi-purpose files in the codebase that should be refactored for better maintainability. Notably, StartScene.js (the main menu scene) is over 900 lines long and mixes background rendering, UI layout, and input handling for seed features in one class. Additionally, certain gameplay mechanics within GameScene – such as the trick and braking system for player stunts – are currently embedded in the scene update, blending concerns of input, physics, and UI feedback
github.com
github.com
. This issue proposes breaking these out: splitting StartScene into smaller modules or scenes (for its background and UI components), and extracting the trick/air-control logic into its own subsystem. By doing so, we further enforce separation of concerns across the codebase, making each piece of functionality easier to work on independently. Why It Matters: These refactors will reduce complexity in key areas outside the core gameplay loop. The start menu is the player’s first impression and likely to evolve (with new options, settings, etc.), so having its code modular will simplify future changes (e.g., adding a settings menu or different background themes). Similarly, the trick system (tucking, parachuting, braking mechanics) is a distinct aspect of gameplay that could be tweaked or expanded (for instance, adding new trick types or rebalancing physics) – isolating it in its own module allows such changes without risking other parts of GameScene. Overall, decomposing large files improves readability and aligns the code with a more modular architecture, which is crucial as we prepare for a larger audience and potential updates post-release. Targets for Refactor:
StartScene Modularization: StartScene.js currently handles a variety of tasks: generating a random seed and storing it, drawing an animated background (stars, mountains, scanlines), creating UI elements like the "START GAME" button and seed input buttons, and handling clipboard operations for copying/pasting seeds
github.com
github.com
. We should split this into logical parts:
Background & Visual Effects: Separate the code that draws the retro background (starfield, mountains, scan lines, etc.) into its own module or helper. For example, a StartSceneBackground class or simply a function in a new file could construct these graphics when called. This would encapsulate all the for-loops and drawing calls for stars and mountains, which are currently cluttering the scene code.
Menu UI and Interactions: Create a module to manage the menu interface – e.g., a MainMenuUI class responsible for creating the “Start Game” button, the seed display, and the copy/paste buttons, and handling their interactive events. This class can also contain the utility methods for clipboard access (copyTextToClipboard) and maybe input validation for pasted seeds. It would use the Phaser scene to add buttons and text, but the layout and event logic would reside in this module instead of in StartScene. The existing createRetroButton helper inside StartScene can be moved to a UI utilities file or integrated into this UI class for reusability.
Seed Management: Right now, StartScene generates a seed and saves it globally (window.gameSeed perhaps) and has copy/paste functionality. We can factor out the seed generation and handling into either the UI module (since it’s part of the menu flow) or a separate small service. For example, a SeedManager could expose generateNewSeed() (using the existing generateGameSeed() from seed-generator.js), and methods to copy or paste seeds using the Clipboard API. StartScene (or the MainMenuUI) would use this service to get a seed and update the display. This separation means if we ever extend seeding (like allowing custom seeds, showing seed validation), it’s all in one place.
Consider Separate Scenes: Another approach (optional) is to divide StartScene into two Phaser Scenes – one purely for the animated background, and one for the UI overlay (menu). Phaser allows multiple scenes to run in parallel and layered. By doing so, the background animation could keep running independently, and the UI scene would handle inputs without worrying about the rendering timing of the background. If the current implementation is performant and simple enough, we might skip this and just keep one StartScene class using sub-modules. But we should design the modules such that moving to separate scenes later would be straightforward (for example, the background module doesn’t rely on StartScene’s update loop too heavily – it could run as its own scene’s update if needed).
Trick/Brake System Module: In GameScene, the logic for tricks (tucking on ground, parachuting in air) and braking (drag on ground, airbrake in air) is handled via a series of if statements in the update loop
github.com
github.com
. This code not only checks input states but also applies physics forces and triggers UI feedback (like calling showToast for “Speed Boost!” or “Parachute!” messages). We should extract this into a dedicated TrickSystem (or “StuntController”) module. Features of this module:
It can hold the state booleans (isTucking, isParachuting, isDragging, isAirBraking) internally rather than as properties of GameScene. GameScene would instantiate this TrickSystem and perhaps pass in references to the player’s physics body and any methods to update UI (or a reference to the HUD/Toast system).
It provides an update method or function, e.g. trickSystem.update(onGround, inputState) which takes the player’s ground state and current input flags (trickAction/brakeAction) each tick. The TrickSystem will decide what to do: set states, apply forces via Matter physics, and inform the HUD of any messages to display. Because it knows when a state starts or stops (e.g., transitioning from not tucking to tucking), it can call something like hud.showToast("Speed Boost!", 1500) via a callback or event.
Physics force application (e.g., applying forward force when tucking, upward force when parachuting, etc.) can be encapsulated in this module. We might pass the Matter Body object for the player into the TrickSystem at construction, so it can call Body.applyForce directly. Alternatively, the TrickSystem could emit events like "applyForce" that GameScene listens to and then applies the force. The direct approach is simpler.
This separation will also allow unit-testing the trick logic by simulating different states without a full Phaser scene (we can pass a mock Body with spies to verify forces applied).
The RotationSystem (for flips) will remain separate; TrickSystem is complementary. We should ensure they don’t conflict – e.g., both might adjust player rotation or velocity, so their interplay should be considered. Possibly, the TrickSystem could incorporate slight integration with RotationSystem (for example, when parachuting, we might disable flip detection or vice versa), but that’s beyond scope – main point is to isolate the code.
Other Candidates: Survey the codebase for any other files over ~400 lines or doing multiple jobs. At this time, GameScene and StartScene are the standouts. Manette.js (~300 lines) handles both keyboard and gamepad input, but it’s already a contained input module – we can leave it as is (just ensure our InputController in Issue 1 uses it appropriately). The test files (like the large puppeteer-tests.js) could be split into smaller files per feature, but that falls under testing (Issue 2) rather than core architecture. We note it as a future improvement for test maintainability (e.g., separate integration test files for menu flow vs. in-game mechanics), but it’s lower priority than game code refactoring.
Plan & Steps:
Refactor StartScene in Stages: Break down StartScene.js by functionality:
Create a new file (e.g., StartSceneBackground.js) and move all code related to drawing the background and effects into it. This could expose a function createBackground(scene) or a class that StartScene can instantiate (e.g., this.background = new StartSceneBackground(this) in create). The background module will use the provided scene to draw graphics (stars, mountains). It might also handle the update of any animations (if, say, we wanted to animate stars; currently they are static except for twinkling via tweens, which the scene already set up).
Create a MainMenuUI.js module/class. It should take the scene in its constructor and have methods to create the UI. You can move the code that was in StartScene.create() for building the menu (text and buttons) into here. For instance, in StartScene’s create, instead of a long sequence of UI setup, you do this.menuUI = new MainMenuUI(this); this.menuUI.createMenu(this.startGame);. The createMenu method inside MainMenuUI will add the Start button, seed display, Copy and Paste buttons, etc. It can accept a callback for starting the game (like this.startGame passed in) to invoke when Start is clicked.
Migrate the event handlers: In MainMenuUI, set up copyButton.on('pointerdown', ...) etc., using the methods moved from StartScene. For clipboard, MainMenuUI can either call a small clipboard helper or directly use the logic. We might move the exact copyTextToClipboard function from StartScene into a standalone util (maybe utils/clipboard.js) to reuse and test it easily. Then MainMenuUI would import that.
Remove the copied code from StartScene. Keep StartScene focused: generating the seed (or better, get it from a SeedManager), and transitioning to GameScene on start.
Ensure data flow: The StartScene’s seed needs to be shared with GameScene (the game uses that seed to initialize terrain randomness). How this is done currently (likely via a global or passing through config) should be maintained or improved. Possibly, after refactor, StartScene could store the chosen seed in a global window.gameSeed (as it does) or in the GameScene’s data. We could use this.scene.start('GameScene', { seed: this.seed }) to pass it properly via scene launch data, which is cleaner than a global. If not already using that, consider it as part of this refactor.
Test the refactored StartScene manually: it should look and behave the same (background visuals intact, buttons functional including copying/pasting seed, and starting the game transitions correctly).
Implement TrickSystem:
Design the interface: likely a class TrickSystem in a new file (e.g., src/game/TrickSystem.js). It might take parameters like (scene, playerSprite) or (playerBody, hud) depending on what it needs direct access to. Alternatively, it could take minimal inputs each update (onGround boolean, etc.) and have references set up for the rest.
Move all relevant properties and logic from GameScene.update into this class. For example, this.isTucking becomes this.tucking property inside TrickSystem. The code that checks if (trickAction active) and applies forces will be in a method like updateTrickState(onGround, trickActionActive, brakeActionActive). Inside that, use the Matter Body of the player (passed in or accessible via scene) to apply forces. Use the scene’s HUD or events to display toasts (the TrickSystem could fire an event like 'trickStarted' with a type ("Speed Boost") that the HUD listens to, or call a provided function directly).
Remove that logic from GameScene. In GameScene.update, instead of a long block for trick/brake, simply call something like this.trickSystem.updateTrickState(this.onGround, this.inputController.isTrickPressed(), this.inputController.isBrakePressed()). Or if the InputController already reflects those as actions, you pass them in.
Ensure to handle the “reset” of states when the keys are released (the else clauses that set isTucking=false etc. when not active). The TrickSystem update method can handle that: if neither action is active, it will clear any active states and restore any modified player properties (like resetting the sled position when parachute ends
github.com
).
Integrate with RotationSystem carefully: The TrickSystem should perhaps not interfere with rotation, but note if the player is parachuting, we apply upward force and preserve horizontal velocity, which might affect rotation dynamics. This is fine; just ensure RotationSystem still gets correct deltaRotation each frame. (We might call RotationSystem.update after TrickSystem.update, so any velocity changes are accounted for, though deltaRotation is based on angular velocity which we don't modify in TrickSystem).
As a bonus, unify how toast messages are invoked. Currently GameScene calls showToast for each trick/brake action start. After refactoring, TrickSystem could call scene.showToast directly. If we moved showToast into the HUD module (Issue 1), then TrickSystem might need a reference to that HUD or an event bus to trigger it. For simplicity, we can give TrickSystem a reference to scene or a specific toastContainer and call it directly. This is acceptable since TrickSystem is closely tied to game logic and UI feedback.
Test & Iterate: After refactoring these:
Run the game to ensure the start menu still appears and works. The refactor should not change any visuals (the neon background and moving stars should render as before) or functionality (the Start button should still start the game, Copy/Paste should still interact with the clipboard and update the seed text).
Play the game to verify that trick actions still function: Press the trick key (e.g., ‘D’ or right arrow) on ground – the player should speed boost and a "Speed Boost!" toast appears; in air, press trick – the parachute slows the fall and a "Parachute!" message appears. Likewise, press brake (e.g., ‘A’ or left arrow) – on ground the drag slows the player, in air the airbrake significantly cuts speed and shows "Dragging!" or appropriate text. Ensure these messages and effects stop when you release the keys.
This manual test is crucial because we are moving time-sensitive logic out of the main update; any slight mistake (like not resetting a flag) could break the feel of the trick system.
Also run the automated tests (Issue 2’s additions will be helpful here). If we wrote unit tests for TrickSystem, run them to confirm they pass. Integration tests for tricks should also pass (or need minor adjustments if how we trigger toasts changed, etc.).
Clean Up and Document: Clean up any leftover commented code in StartScene or GameScene that is no longer needed. Update documentation to reflect these new modules:
Perhaps add comments at the top of StartScene.js explaining that it now delegates to StartSceneBackground and MainMenuUI.
In GameScene.js, note that trick logic is handled by TrickSystem.
If any global variables were eliminated (like using scene data to pass the seed instead of window.gameSeed), note that improvement.
In a future contributor guide, the project’s structure section should mention these subsystems.
Expected Outcome (After):
StartScene.js will be much shorter (only core logic and orchestration). The background drawing code and menu UI code will reside in separate modules (e.g., StartSceneBackground.js might be ~100-200 lines, MainMenuUI.js another ~300 lines, splitting the original 950-line StartScene). These modules encapsulate their tasks, making the start menu code easier to navigate. For example, if a developer needs to tweak the menu layout, they can go straight to MainMenuUI.js instead of scrolling through hundreds of lines of unrelated drawing code.
The game’s start screen behavior remains the same, but now we have a clear structure: one part responsible for visuals, another for interactive UI. This also opens the possibility to reuse the retro UI style elsewhere (since createRetroButton and similar can be general utilities).
TrickSystem: GameScene will have an instance of TrickSystem and delegate all stunt-related updates to it. The code for applying trick forces and showing messages is isolated. This means, for instance, if we want to adjust the airbrake strength or add a new “super jump” trick, we can do it in TrickSystem.js without touching the main GameScene logic. The separation also helps in testing and tuning – we could easily disable the entire trick system by not updating it, to see baseline physics, or swap it out if we ever implement an alternate stunt mechanic.
Overall, no functionality change from a player’s perspective – this is purely an internal reorganization to support maintainability.
Verification Checklist:
StartScene works as before: Launch the game and verify the start screen renders correctly (background effects visible, UI elements in place). Clicking “START GAME” should smoothly transition to the GameScene (with the fade-out effect still functioning). Test the seed Copy/Paste: clicking “Copy Seed” should copy the current seed (check by pasting somewhere external), and “Paste Seed” should replace the seed text if a valid seed is in clipboard. Both should give visual feedback (text flash or color change) as before.
StartScene code size reduction: Confirm that StartScene.js is significantly shorter and cleaner. Ideally, it should mostly contain high-level calls (background.create(...), menuUI.create(...), etc.) and not long blocks of drawing or UI code. The new module files (StartSceneBackground.js, MainMenuUI.js, etc.) should logically contain the moved code. This can be reviewed via line counts or simply by reading to ensure each module has a single responsibility.
Trick/Brake functionality intact: In gameplay, perform all four actions – tuck (ground trick), parachute (air trick), drag (ground brake), airbrake (air brake). Each should trigger at the appropriate time with the expected effect on physics and a toast message. Releasing the keys should end the effect (e.g., coming out of a tuck returns to normal speed, parachute release restores gravity, etc.). Compare with pre-refactor behavior to ensure no regressions (the player’s feel and timing should be the same).
No new errors in console for the refactored parts. Specifically, ensure that scene transitions still find everything (e.g., no undefined because we forgot to pass seed to GameScene, no missing property errors in TrickSystem). Also test resizing on the start menu if that was supported (the background graphics should still cover the screen after a window resize event, etc.).
Automated tests pass: Run the full test suite. Unit tests for StartScene (if any) might need updates if we changed how seed is passed or how StartScene calls things. Update them accordingly. The integration tests for copying seed or starting game should all pass as before (they might not even need changes if we kept the outward behavior identical). The trick system tests should pass using the new TrickSystem module (if we wrote new ones, they should cover the scenarios we manually tested).
Code review approval: Have at least one other developer review the changes. Because these are architectural refactors, a second pair of eyes can ensure we didn’t unintentionally change logic. The reviewer should confirm that the new module boundaries make sense and that the code is easier to follow.
Future extension readiness: This is somewhat subjective, but verify that the new structure indeed makes future work easier. For instance, imagine adding a “Settings” button to the start menu – with the refactor, one would do this in MainMenuUI.js fairly easily. Or imagine adjusting trick physics – now one would go to TrickSystem.js. If these tasks seem straightforward with the new code layout, then the refactor succeeded in its goal of preparing the project for growth and polish.