From 19b86ebdbcae38f4b69a742a7636401f796b848a Mon Sep 17 00:00:00 2001 From: Hector Bailey Date: Tue, 15 Sep 2026 19:22:47 +0100 Subject: [PATCH] F4 latches the side panel away, and holding Space slides it away The original's keyboard has two ways to see the map under the side panel: F4 flips bit 7 of the display-options word and the panel slides off the left edge and stays there, and holding Space slides it away for as long as the key is down, unless the cursor is on the panel itself. The updater at 0x4948E0 gives the motion: each frame the position moves a quarter of what is left of the 125 pixels, never less than one, so the panel starts fast and eases in; the Panel sound plays as it leaves an end of its travel and Options as it arrives at one. RWE moves the panel and the minimap with the slide and lets the world viewport widen under them. The top and bottom bars stay where they are: their labels and figures sit at fixed positions, and sliding their tiles under them only garbled the two. Presentation only. Enter and h, the message bar and the share screen, wait for chat and sharing to exist; the issue stays open for them. Issue: #30 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJCbwC9MGZnM6erMDKptun --- src/rwe/LoadingScene.cpp | 2 + src/rwe/game/GameScene.cpp | 3 +- src/rwe/game/GameScene.h | 21 ++++++++ src/rwe/game/GameScene_commands.cpp | 2 +- src/rwe/game/GameScene_input.cpp | 77 +++++++++++++++++++++++++++++ src/rwe/game/GameScene_render.cpp | 11 +++-- src/rwe/game/InGameSoundsInfo.h | 3 ++ src/rwe/ui/UiComponent.h | 7 +++ 8 files changed, 119 insertions(+), 7 deletions(-) diff --git a/src/rwe/LoadingScene.cpp b/src/rwe/LoadingScene.cpp index 33c14c290..754bee9bb 100644 --- a/src/rwe/LoadingScene.cpp +++ b/src/rwe/LoadingScene.cpp @@ -472,6 +472,8 @@ namespace rwe sounds.okToBuild = lookUpSound("OKTOBUILD"); sounds.notOkToBuild = lookUpSound("NOTOKTOBUILD"); sounds.selectMultipleUnits = lookUpSound("SelectMultipleUnits"); + sounds.panel = lookUpSound("Panel"); + sounds.options = lookUpSound("Options"); auto consoleFont = sceneContext.textureService->getFont("fonts/CONSOLE.FNT"); // The original loads exactly two in-game fonts (0x42A320): COMIX for diff --git a/src/rwe/game/GameScene.cpp b/src/rwe/game/GameScene.cpp index 7a8d307f6..6599e3e37 100644 --- a/src/rwe/game/GameScene.cpp +++ b/src/rwe/game/GameScene.cpp @@ -342,6 +342,7 @@ namespace rwe } updateMusic(); + updatePanelSlide(); // Pause halts simulation tick dispatch by not advancing the // scaled-time accumulator. Speed scales the accumulator using @@ -415,7 +416,7 @@ namespace rwe // convert that to the world, then set the camera's position to there // (clamped to map bounds) - auto minimapToWorld = minimapToWorldMatrix(simulation.terrain, minimapRect); + auto minimapToWorld = minimapToWorldMatrix(simulation.terrain, slidMinimapRect()); auto mousePos = getMousePosition(); auto worldPos = minimapToWorld * Vector3f(static_cast(mousePos.x) + 0.5f, static_cast(mousePos.y) + 0.5, 0.0f); diff --git a/src/rwe/game/GameScene.h b/src/rwe/game/GameScene.h index db65ff8f9..6665a1b07 100644 --- a/src/rwe/game/GameScene.h +++ b/src/rwe/game/GameScene.h @@ -298,6 +298,22 @@ namespace rwe bool leftShiftDown{false}; bool rightShiftDown{false}; + /** + * The side panel's slide, 0 with the panel in place and + * PanelSlideExtent with it slid off the left edge. F4 latches it + * away; holding Space slides it away while the key is down and the + * cursor is not on the panel itself. The original's updater + * (0x4948E0, TOTALA-EXE.md "the panel-slide updater") moves it a + * quarter of the remaining distance a frame and never less than a + * pixel, plays the Panel sound as it leaves an end and Options as it + * arrives at one, and 125 is the panel's width less its border. + * Presentation only: the world viewport grows under it. + */ + static constexpr int PanelSlideExtent = 125; + int panelSlide{0}; + bool panelSlideLatched{false}; + bool spaceDown{false}; + struct CameraControlStateFree { }; @@ -845,6 +861,11 @@ namespace rwe void addBattlePoints(int points); void updateMusic(); + /** Steps the side panel's slide one frame; see panelSlide. */ + void updatePanelSlide(); + /** The minimap's rectangle where it is drawn this frame, slid with the panel. */ + Rectangle2f slidMinimapRect() const; + /** What size the world render textures were made at, so a window resize remakes them. */ std::pair worldRenderTextureSize{0, 0}; diff --git a/src/rwe/game/GameScene_commands.cpp b/src/rwe/game/GameScene_commands.cpp index 88e7c7795..68e197a7c 100644 --- a/src/rwe/game/GameScene_commands.cpp +++ b/src/rwe/game/GameScene_commands.cpp @@ -106,7 +106,7 @@ namespace rwe bool GameScene::isCursorOverMinimap() const { auto mousePos = getMousePosition(); - return minimapRect.contains(mousePos.x, mousePos.y); + return slidMinimapRect().contains(mousePos.x, mousePos.y); } bool GameScene::isCursorOverWorld() const diff --git a/src/rwe/game/GameScene_input.cpp b/src/rwe/game/GameScene_input.cpp index 6c83451e2..f5412f287 100644 --- a/src/rwe/game/GameScene_input.cpp +++ b/src/rwe/game/GameScene_input.cpp @@ -113,6 +113,19 @@ namespace rwe { showDebugWindow = !showDebugWindow; } + else if (keysym.key == SDLK_F4) + { + // Toggles bit 7 of the display-options word (0x49639D), the one + // bit of it with no registry name: the side panel slides away + // and stays away until the next press. See panelSlide. + panelSlideLatched = !panelSlideLatched; + } + else if (keysym.key == SDLK_SPACE) + { + // Polled, not dispatched, in the original (0x494942): while it + // is held the panel slides away, and back when it is let go. + spaceDown = true; + } else if (keysym.key == SDLK_F1) { helpVisible = !helpVisible; @@ -425,6 +438,70 @@ namespace rwe { rightShiftDown = false; } + else if (keysym.key == SDLK_SPACE) + { + spaceDown = false; + } + } + + void GameScene::updatePanelSlide() + { + // 0x4948E0. With the F4 bit set the position runs to the extent; + // with it clear it runs to zero, unless Space is held and the cursor + // is not on the panel's own gadget, which runs it to the extent + // again. Each frame moves a quarter of what is left, never less + // than a pixel, so the panel starts fast and eases in. + auto mouse = getMousePosition(); + auto overPanel = mouse.x >= 0 && mouse.x < GuiSizeLeft - panelSlide; + auto wantHidden = panelSlideLatched || (spaceDown && !overPanel); + + auto before = panelSlide; + if (wantHidden && panelSlide < PanelSlideExtent) + { + if (panelSlide == 0 && sounds.panel) + { + playUiSound(*sounds.panel); + } + panelSlide += std::max(1, (PanelSlideExtent - panelSlide) / 4); + if (panelSlide >= PanelSlideExtent) + { + panelSlide = PanelSlideExtent; + if (sounds.options) + { + playUiSound(*sounds.options); + } + } + } + else if (!wantHidden && panelSlide > 0) + { + if (panelSlide == PanelSlideExtent && sounds.panel) + { + playUiSound(*sounds.panel); + } + panelSlide -= std::max(1, panelSlide / 4); + if (panelSlide <= 0) + { + panelSlide = 0; + if (sounds.options) + { + playUiSound(*sounds.options); + } + } + } + + if (panelSlide != before && guiVisible) + { + worldViewport.setInset(GuiSizeLeft - panelSlide, GuiSizeTop, GuiSizeRight, GuiSizeBottom); + } + if (currentPanel) + { + currentPanel->setPosition(-panelSlide, currentPanel->getY()); + } + } + + Rectangle2f GameScene::slidMinimapRect() const + { + return Rectangle2f::fromTopLeft(minimapRect.left() - static_cast(panelSlide), minimapRect.top(), minimapRect.width(), minimapRect.height()); } void GameScene::onMouseDown(MouseButtonEvent event) diff --git a/src/rwe/game/GameScene_render.cpp b/src/rwe/game/GameScene_render.cpp index 4b5615dfa..3cb9629be 100644 --- a/src/rwe/game/GameScene_render.cpp +++ b/src/rwe/game/GameScene_render.cpp @@ -528,15 +528,16 @@ namespace rwe void GameScene::renderMinimap() { + auto minimapDrawRect = slidMinimapRect(); // draw minimap - chromeUiRenderService.drawSpriteAbs(minimapRect, *minimap); + chromeUiRenderService.drawSpriteAbs(minimapDrawRect, *minimap); if (fogSprite) { - chromeUiRenderService.drawSpriteAbs(minimapRect, *fogSprite); + chromeUiRenderService.drawSpriteAbs(minimapDrawRect, *fogSprite); } auto cameraInverse = computeInverseViewProjectionMatrix(worldCameraState, worldViewport.width(), worldViewport.height()); - auto worldToMinimap = worldToMinimapMatrix(simulation.terrain, minimapRect); + auto worldToMinimap = worldToMinimapMatrix(simulation.terrain, minimapDrawRect); // draw minimap dots for (const auto& [unitId, unit] : simulation.units) @@ -743,7 +744,7 @@ namespace rwe } chromeUiRenderService.drawLines( - buildMinimapRing(Vector2f(centre.x, centre.y), radius, minimapRect, false, 0), + buildMinimapRing(Vector2f(centre.x, centre.y), radius, slidMinimapRect(), false, 0), color); } } @@ -804,7 +805,7 @@ namespace rwe auto parity = dashed && ((sceneTime.value / minimapBlinkTicks) % 2 == 1) ? 1 : 0; chromeUiRenderService.drawLines( - buildMinimapRing(Vector2f(centre.x, centre.y), radius, minimapRect, dashed, parity), + buildMinimapRing(Vector2f(centre.x, centre.y), radius, slidMinimapRect(), dashed, parity), Color(255, 255, 255)); } } diff --git a/src/rwe/game/InGameSoundsInfo.h b/src/rwe/game/InGameSoundsInfo.h index 6a86746c5..fa1fc5699 100644 --- a/src/rwe/game/InGameSoundsInfo.h +++ b/src/rwe/game/InGameSoundsInfo.h @@ -21,5 +21,8 @@ namespace rwe std::optional notOkToBuild; std::optional selectMultipleUnits; + /** The side panel leaving an end of its slide, and arriving at one. */ + std::optional panel; + std::optional options; }; } diff --git a/src/rwe/ui/UiComponent.h b/src/rwe/ui/UiComponent.h index faa812036..ec8e64965 100644 --- a/src/rwe/ui/UiComponent.h +++ b/src/rwe/ui/UiComponent.h @@ -68,6 +68,13 @@ namespace rwe int getX() { return posX; } int getY() { return posY; } + /** Moves the component; a panel's children move with it, drawn and hit-tested alike. */ + void setPosition(int x, int y) + { + posX = x; + posY = y; + } + virtual void mouseDown(MouseButtonEvent /*event*/) {} virtual void mouseUp(MouseButtonEvent /*event*/) {}