diff --git a/.gitignore b/.gitignore index 61be22e..e40ff40 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ Thumbs.db *.uid .godot/ .import/ +.codegraph/ diff --git a/CMakeLists.txt b/CMakeLists.txt index dbe40ea..4a24b84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,8 +60,19 @@ target_link_libraries(mana_core PUBLIC PkgConfig::PUGIXML) target_compile_options(mana_core PRIVATE -Wall -Wextra -Wpedantic) add_executable(mana_pet_world + src/app/LogicalViewport.cpp src/app/main.cpp src/battle/BattleScene.cpp ) target_link_libraries(mana_pet_world PRIVATE mana_core PkgConfig::RAYLIB) target_compile_options(mana_pet_world PRIVATE -Wall -Wextra -Wpedantic) + +enable_testing() + +add_executable(logical_viewport_test + tests/app/LogicalViewportTest.cpp + src/app/LogicalViewport.cpp +) +target_include_directories(logical_viewport_test PRIVATE src/app) +target_compile_options(logical_viewport_test PRIVATE -Wall -Wextra -Wpedantic) +add_test(NAME logical_viewport_test COMMAND logical_viewport_test) diff --git a/src/app/LogicalViewport.cpp b/src/app/LogicalViewport.cpp new file mode 100644 index 0000000..61cb871 --- /dev/null +++ b/src/app/LogicalViewport.cpp @@ -0,0 +1,42 @@ +#include "LogicalViewport.h" + +namespace mana::app { + +LogicalViewport ComputeLogicalViewport(int windowWidth, int windowHeight) +{ + const float safeWindowWidth = static_cast(std::max(1, windowWidth)); + const float safeWindowHeight = static_cast(std::max(1, windowHeight)); + const float scale = std::min( + safeWindowWidth / static_cast(kLogicalScreenWidth), + safeWindowHeight / static_cast(kLogicalScreenHeight)); + const float width = static_cast(kLogicalScreenWidth) * scale; + const float height = static_cast(kLogicalScreenHeight) * scale; + return { + (safeWindowWidth - width) * 0.5f, + (safeWindowHeight - height) * 0.5f, + width, + height, + scale, + }; +} + +LogicalPoint WindowToLogicalPoint(float windowX, float windowY, const LogicalViewport& viewport) +{ + const float safeScale = std::max(0.0001f, viewport.scale); + return { + (windowX - viewport.x) / safeScale, + (windowY - viewport.y) / safeScale, + }; +} + +WindowRect LogicalRectToWindowRect(LogicalRect rect, const LogicalViewport& viewport) +{ + return { + viewport.x + rect.x * viewport.scale, + viewport.y + rect.y * viewport.scale, + rect.width * viewport.scale, + rect.height * viewport.scale, + }; +} + +} // namespace mana::app diff --git a/src/app/LogicalViewport.h b/src/app/LogicalViewport.h new file mode 100644 index 0000000..bf348fd --- /dev/null +++ b/src/app/LogicalViewport.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +namespace mana::app { + +constexpr int kLogicalScreenWidth = 1280; +constexpr int kLogicalScreenHeight = 720; +constexpr int kMinimumWindowWidth = 640; +constexpr int kMinimumWindowHeight = 360; + +struct LogicalViewport { + float x = 0.0f; + float y = 0.0f; + float width = static_cast(kLogicalScreenWidth); + float height = static_cast(kLogicalScreenHeight); + float scale = 1.0f; +}; + +struct LogicalPoint { + float x = 0.0f; + float y = 0.0f; +}; + +struct LogicalRect { + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; +}; + +struct WindowRect { + float x = 0.0f; + float y = 0.0f; + float width = 0.0f; + float height = 0.0f; +}; + +LogicalViewport ComputeLogicalViewport(int windowWidth, int windowHeight); +LogicalPoint WindowToLogicalPoint(float windowX, float windowY, const LogicalViewport& viewport); +WindowRect LogicalRectToWindowRect(LogicalRect rect, const LogicalViewport& viewport); + +} // namespace mana::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 1f23e7b..fc86734 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -7,6 +7,7 @@ #include "InteractableVisual.h" #include "InventoryUiModel.h" #include "ItemIconCatalog.h" +#include "LogicalViewport.h" #include "MapAtmosphere.h" #include "MinimapAsset.h" #include "MusicAssets.h" @@ -50,6 +51,20 @@ namespace { +using mana::app::kLogicalScreenHeight; +using mana::app::kLogicalScreenWidth; +using mana::app::kMinimumWindowHeight; +using mana::app::kMinimumWindowWidth; + +Camera2D LogicalUiCamera(const mana::app::LogicalViewport& viewport) +{ + Camera2D camera{}; + camera.offset = {viewport.x, viewport.y}; + camera.target = {0.0f, 0.0f}; + camera.zoom = viewport.scale; + return camera; +} + struct TextureEntry { Texture2D texture{}; bool loaded = false; @@ -2960,22 +2975,25 @@ void DrawTexturedProgressBar(Runtime& rt, Rectangle bounds, float ratio, Color t } } -void DrawMiniMap(Runtime& rt, Font font) +void DrawMiniMap(Runtime& rt, Font font, const mana::app::LogicalViewport& logicalViewport) { (void)font; const Rectangle panel{8.0f, 8.0f, 160.0f, 96.0f}; - const Rectangle viewport{panel.x + 5.0f, panel.y + 5.0f, panel.width - 10.0f, panel.height - 10.0f}; + const Rectangle mapViewport{panel.x + 5.0f, panel.y + 5.0f, panel.width - 10.0f, panel.height - 10.0f}; DrawMinimapPanel(rt, panel); const float mapWidth = static_cast(std::max(1, rt.map.width * rt.map.tileWidth)); const float mapHeight = static_cast(std::max(1, rt.map.height * rt.map.tileHeight)); + const mana::app::WindowRect scissor = mana::app::LogicalRectToWindowRect( + {mapViewport.x, mapViewport.y, mapViewport.width, mapViewport.height}, + logicalViewport); BeginScissorMode( - static_cast(viewport.x), - static_cast(viewport.y), - static_cast(viewport.width), - static_cast(viewport.height)); + static_cast(std::floor(scissor.x)), + static_cast(std::floor(scissor.y)), + static_cast(std::ceil(scissor.width)), + static_cast(std::ceil(scissor.height))); if (!rt.minimapPath.empty() && std::filesystem::exists(rt.minimapPath)) { Texture2D& minimap = LoadTextureCached(rt, rt.minimapPath); @@ -2989,14 +3007,14 @@ void DrawMiniMap(Runtime& rt, Font font) playerRatio.x * textureW - 5.0f, playerRatio.y * textureH - 5.0f, }; - const float sourceW = std::min(viewport.width, textureW); - const float sourceH = std::min(viewport.height, textureH); - const float scrollX = std::clamp(minimapPlayerPos.x - viewport.width * 0.5f, 0.0f, std::max(0.0f, textureW - sourceW)); - const float scrollY = std::clamp(minimapPlayerPos.y - viewport.height * 0.5f, 0.0f, std::max(0.0f, textureH - sourceH)); + const float sourceW = std::min(mapViewport.width, textureW); + const float sourceH = std::min(mapViewport.height, textureH); + const float scrollX = std::clamp(minimapPlayerPos.x - mapViewport.width * 0.5f, 0.0f, std::max(0.0f, textureW - sourceW)); + const float scrollY = std::clamp(minimapPlayerPos.y - mapViewport.height * 0.5f, 0.0f, std::max(0.0f, textureH - sourceH)); const Rectangle sourceRect{scrollX, scrollY, sourceW, sourceH}; const Rectangle mapRect{ - viewport.x, - viewport.y, + mapViewport.x, + mapViewport.y, sourceW, sourceH, }; @@ -3008,8 +3026,8 @@ void DrawMiniMap(Runtime& rt, Font font) 0.0f, WHITE); const Vector2 playerPoint{ - viewport.x + minimapPlayerPos.x - scrollX + 5.0f, - viewport.y + minimapPlayerPos.y - scrollY + 5.0f, + mapViewport.x + minimapPlayerPos.x - scrollX + 5.0f, + mapViewport.y + minimapPlayerPos.y - scrollY + 5.0f, }; if (const std::optional target = CurrentQuestTargetPosition(rt)) { const Vector2 targetRatio{ @@ -3017,10 +3035,10 @@ void DrawMiniMap(Runtime& rt, Font font) std::clamp(target->y / mapHeight, 0.0f, 1.0f), }; const Vector2 targetPoint{ - viewport.x + targetRatio.x * textureW - scrollX, - viewport.y + targetRatio.y * textureH - scrollY, + mapViewport.x + targetRatio.x * textureW - scrollX, + mapViewport.y + targetRatio.y * textureH - scrollY, }; - if (CheckCollisionPointRec(targetPoint, viewport)) { + if (CheckCollisionPointRec(targetPoint, mapViewport)) { DrawCircleV(targetPoint, 6.0f, Color{139, 214, 146, 235}); DrawCircleLines(static_cast(targetPoint.x), static_cast(targetPoint.y), 10.0f, Color{255, 255, 221, 255}); DrawCircleLines(static_cast(targetPoint.x), static_cast(targetPoint.y), 15.0f, Color{139, 214, 146, 170}); @@ -3295,10 +3313,10 @@ void DrawDebugCoordinates(Runtime& rt, Font font) DrawTextCn(font, CompactSlotLabel(warpLine, 58).c_str(), {panel.x + 14.0f, panel.y + 102.0f}, 14.0f, Color{205, 208, 190, 245}); } -void DrawUi(Runtime& rt, Font font) +void DrawUi(Runtime& rt, Font font, const mana::app::LogicalViewport& logicalViewport) { if (rt.minimapVisible) { - DrawMiniMap(rt, font); + DrawMiniMap(rt, font, logicalViewport); } const Rectangle tracker{846.0f, 18.0f, 416.0f, 86.0f}; DrawRectangleRounded(tracker, 0.05f, 6, Color{244, 231, 186, 222}); @@ -7024,7 +7042,9 @@ int main(int argc, char** argv) PrewarmMapEntityState(rt, IndoorAliasMapName()); RefreshProgress(rt); - InitWindow(1280, 720, "玛纳宠物世界"); + SetConfigFlags(FLAG_WINDOW_RESIZABLE); + InitWindow(kLogicalScreenWidth, kLogicalScreenHeight, "玛纳宠物世界"); + SetWindowMinSize(kMinimumWindowWidth, kMinimumWindowHeight); SetExitKey(0); rt.graphicsReady = true; InitAudioDevice(); @@ -7035,12 +7055,20 @@ int main(int argc, char** argv) PrewarmRuntimeGraphics(rt); Camera2D camera{}; - camera.offset = {640.0f, 360.0f}; + camera.offset = {static_cast(kLogicalScreenWidth) * 0.5f, static_cast(kLogicalScreenHeight) * 0.5f}; camera.zoom = 1.55f; rt.observerCamera = rt.player; rt.observerZoom = camera.zoom; while (!WindowShouldClose()) { + const mana::app::LogicalViewport viewport = mana::app::ComputeLogicalViewport(GetScreenWidth(), GetScreenHeight()); + SetMouseOffset( + static_cast(std::round(-viewport.x)), + static_cast(std::round(-viewport.y))); + SetMouseScale( + static_cast(kLogicalScreenWidth) / std::max(1.0f, viewport.width), + static_cast(kLogicalScreenHeight) / std::max(1.0f, viewport.height)); + const Camera2D uiCamera = LogicalUiCamera(viewport); const float dt = GetFrameTime(); if (rt.currentMusicLoaded) { UpdateMusicStream(rt.currentMusic); @@ -7088,17 +7116,24 @@ int main(int argc, char** argv) ProcessTexturePrewarmQueue(rt, 2, 0.0015); camera.target = rt.mode == Mode::Observer ? rt.observerCamera : rt.player; + camera.offset = {static_cast(GetScreenWidth()) * 0.5f, static_cast(GetScreenHeight()) * 0.5f}; camera.zoom = rt.mode == Mode::Observer ? rt.observerZoom : 1.55f; BeginDrawing(); ClearBackground(BLACK); if (rt.mode == Mode::TitleMenu) { + BeginMode2D(uiCamera); DrawTitleMenu(rt, font); + EndMode2D(); } else if (rt.mode == Mode::TitleHelp) { + BeginMode2D(uiCamera); DrawTitleHelp(rt, font); + EndMode2D(); } else if (rt.mode == Mode::Battle) { + BeginMode2D(uiCamera); DrawBattle(rt, font); + EndMode2D(); } else { BeginMode2D(camera); struct VisibleMapDraw { @@ -7269,7 +7304,8 @@ int main(int argc, char** argv) EndMode2D(); DrawMapAtmosphere(rt); - DrawUi(rt, font); + BeginMode2D(uiCamera); + DrawUi(rt, font, viewport); if (rt.nearbyWarp && rt.mode == Mode::Explore) { DrawInteractionPrompt(rt, font, "进入", rt.warpPrompt); } else if (rt.nearbyNpc && rt.mode == Mode::Explore) { @@ -7301,6 +7337,7 @@ int main(int argc, char** argv) DrawPanel(rt, 470, 610, 340, 54); DrawTextCn(font, "观察模式", {590, 626}, 22.0f, RAYWHITE); } + EndMode2D(); } DrawWakeBlinkOverlay(rt); diff --git a/src/battle/BattleScene.cpp b/src/battle/BattleScene.cpp index 3ace71e..fcc981d 100644 --- a/src/battle/BattleScene.cpp +++ b/src/battle/BattleScene.cpp @@ -648,10 +648,7 @@ BattleUiCommand ReadBattleUiCommand(BattleMenuMode& mode, int& selectedIndex, in }; const auto logicalMousePosition = []() { - const Vector2 mouse = GetMousePosition(); - const float screenW = static_cast(std::max(1, GetScreenWidth())); - const float screenH = static_cast(std::max(1, GetScreenHeight())); - return Vector2{mouse.x * 1280.0f / screenW, mouse.y * 720.0f / screenH}; + return GetMousePosition(); }; const auto panelCellAt = [](Rectangle panel, Vector2 point) { diff --git a/submission/screenshot/image0.png b/submission/screenshot/image0.png new file mode 100644 index 0000000..a486fd1 Binary files /dev/null and b/submission/screenshot/image0.png differ diff --git a/submission/screenshot/image1.png b/submission/screenshot/image1.png new file mode 100644 index 0000000..970a22a Binary files /dev/null and b/submission/screenshot/image1.png differ diff --git a/submission/screenshot/image2.png b/submission/screenshot/image2.png new file mode 100644 index 0000000..3cb9edf Binary files /dev/null and b/submission/screenshot/image2.png differ diff --git a/submission/screenshot/image3.png b/submission/screenshot/image3.png new file mode 100644 index 0000000..fdcb568 Binary files /dev/null and b/submission/screenshot/image3.png differ