diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a24b84..8d398df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ target_compile_options(mana_core PRIVATE -Wall -Wextra -Wpedantic) add_executable(mana_pet_world src/app/LogicalViewport.cpp + src/app/TitleMenu.cpp src/app/main.cpp src/battle/BattleScene.cpp ) @@ -69,10 +70,35 @@ 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) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/LogicalViewportTest.cpp") + 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) +endif() + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/TitleMenuTest.cpp") + add_executable(title_menu_test + tests/app/TitleMenuTest.cpp + src/app/TitleMenu.cpp + ) + target_include_directories(title_menu_test PRIVATE src/app) + target_compile_options(title_menu_test PRIVATE -Wall -Wextra -Wpedantic) + add_test(NAME title_menu_test COMMAND title_menu_test) +endif() + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/BattleEncounterTest.cpp") + add_executable(battle_encounter_test + tests/app/BattleEncounterTest.cpp + src/app/LogicalViewport.cpp + src/app/TitleMenu.cpp + src/battle/BattleScene.cpp + ) + target_include_directories(battle_encounter_test PRIVATE src/app) + target_link_libraries(battle_encounter_test PRIVATE mana_core PkgConfig::RAYLIB) + target_compile_options(battle_encounter_test PRIVATE -Wall -Wextra -Wpedantic) + add_test(NAME battle_encounter_test COMMAND battle_encounter_test) +endif() diff --git a/src/app/TitleMenu.cpp b/src/app/TitleMenu.cpp new file mode 100644 index 0000000..99ec535 --- /dev/null +++ b/src/app/TitleMenu.cpp @@ -0,0 +1,33 @@ +#include "TitleMenu.h" + +namespace mana::app { + +std::vector BuildTitleMenuItems(bool hasSave) +{ + std::vector items; + if (hasSave) { + items.push_back({ + TitleMenuAction::ContinueGame, + "继续游戏", + true, + }); + } + items.push_back({ + TitleMenuAction::NewGame, + "开始新游戏", + true, + }); + items.push_back({ + TitleMenuAction::Help, + "帮助", + true, + }); + items.push_back({ + TitleMenuAction::Quit, + "退出游戏", + true, + }); + return items; +} + +} // namespace mana::app diff --git a/src/app/TitleMenu.h b/src/app/TitleMenu.h new file mode 100644 index 0000000..f5610cf --- /dev/null +++ b/src/app/TitleMenu.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace mana::app { + +enum class TitleMenuAction { + ContinueGame, + NewGame, + Help, + Quit +}; + +struct TitleMenuItem { + TitleMenuAction action = TitleMenuAction::ContinueGame; + std::string label; + bool enabled = true; +}; + +std::vector BuildTitleMenuItems(bool hasSave); + +} // namespace mana::app diff --git a/src/app/main.cpp b/src/app/main.cpp index fc86734..5339246 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -17,6 +17,7 @@ #include "ScriptedInteractable.h" #include "SoundAssets.h" #include "SpriteAnimation.h" +#include "TitleMenu.h" #include "TmxMap.h" #include "TmxMapPool.h" #include "TmxWorld.h" @@ -55,6 +56,8 @@ using mana::app::kLogicalScreenHeight; using mana::app::kLogicalScreenWidth; using mana::app::kMinimumWindowHeight; using mana::app::kMinimumWindowWidth; +using mana::app::TitleMenuAction; +using mana::app::TitleMenuItem; Camera2D LogicalUiCamera(const mana::app::LogicalViewport& viewport) { @@ -184,12 +187,6 @@ enum class InventoryPage { Crafting }; -enum class TitleMenuAction { - ContinueGame, - NewGame, - Help -}; - enum class PauseMenuAction { Resume, Save, @@ -197,12 +194,6 @@ enum class PauseMenuAction { Quit }; -struct TitleMenuItem { - TitleMenuAction action = TitleMenuAction::ContinueGame; - std::string label; - bool enabled = true; -}; - struct PauseMenuItem { PauseMenuAction action = PauseMenuAction::Resume; std::string label; @@ -2650,25 +2641,7 @@ std::optional LoadTitleSavePreview(const Runtime& rt) std::vector BuildTitleMenuItems(const Runtime& rt) { const bool hasSave = LoadTitleSavePreview(rt).has_value(); - std::vector items; - if (hasSave) { - items.push_back({ - TitleMenuAction::ContinueGame, - "继续游戏", - true, - }); - } - items.push_back({ - TitleMenuAction::NewGame, - "开始新游戏", - true, - }); - items.push_back({ - TitleMenuAction::Help, - "帮助", - true, - }); - return items; + return mana::app::BuildTitleMenuItems(hasSave); } void DrawTitleButton(Runtime& rt, Font font, const TitleMenuItem& item, Rectangle bounds, bool selected) @@ -2713,15 +2686,20 @@ void DrawTitleMenu(Runtime& rt, Font font) DrawTextCn(font, "托诺里宠物世界", {86.0f, 70.0f}, 48.0f, Color{255, 255, 221, 255}); DrawTextCn(font, "Tonori Pet World", {90.0f, 126.0f}, 24.0f, Color{245, 219, 132, 255}); - const Rectangle menuPanel{74.0f, 244.0f, 500.0f, 338.0f}; + const std::vector items = BuildTitleMenuItems(rt); + rt.selectedTitleSlot = std::clamp(rt.selectedTitleSlot, 0, static_cast(items.size()) - 1); + + const float rowStartY = 330.0f; + const float rowSpacing = 78.0f; + const float rowHeight = 62.0f; + const float lastRowBottom = rowStartY + static_cast(std::max(0, static_cast(items.size()) - 1)) * rowSpacing + rowHeight; + const Rectangle menuPanel{74.0f, 244.0f, 500.0f, std::max(338.0f, lastRowBottom + 32.0f - 244.0f)}; DrawWindowPanel(rt, menuPanel); DrawIcon(rt, "icon/map.png", {106.0f, 266.0f}, 30.0f); DrawTextCn(font, "开始界面", {146.0f, 265.0f}, 27.0f, Color{255, 255, 221, 255}); - std::vector items = BuildTitleMenuItems(rt); - rt.selectedTitleSlot = std::clamp(rt.selectedTitleSlot, 0, static_cast(items.size()) - 1); for (int i = 0; i < static_cast(items.size()); ++i) { - const Rectangle row{110.0f, 330.0f + static_cast(i) * 78.0f, 420.0f, 62.0f}; + const Rectangle row{110.0f, rowStartY + static_cast(i) * rowSpacing, 420.0f, rowHeight}; DrawTitleButton(rt, font, items[static_cast(i)], row, i == rt.selectedTitleSlot); } @@ -2742,7 +2720,7 @@ void DrawTitleMenu(Runtime& rt, Font font) } if (rt.titleNewGameConfirm) { - DrawTextCn(font, "再次确认将覆盖当前存档", {112.0f, 558.0f}, 17.0f, Color{245, 219, 132, 255}); + DrawTextCn(font, "再次确认将覆盖当前存档", {112.0f, lastRowBottom + 12.0f}, 17.0f, Color{245, 219, 132, 255}); } } @@ -4487,6 +4465,7 @@ void StartBattle(Runtime& rt, int petIndex, bool playerFirstAction = false) RestoreActivePetForRetreat(rt); rt.battle.player = rt.team.pets.empty() ? MakePet("Lulea", 30, 7) : rt.team.pets.front(); rt.battle.wild = rt.wildPets[petIndex].pet; + ScheduleMonsterRespawn(rt, rt.wildPets[petIndex]); RegisterPetSeen(rt.petJournal, rt.battle.wild.name); ApplyQuestEvent(rt.questRuntime, QuestEvent::SawPet(rt.battle.wild.name)); rt.battle.message = "野外宠物出现了"; @@ -5734,6 +5713,10 @@ void ExecuteTitleMenuAction(Runtime& rt, TitleMenuAction action, bool enabled) rt.titleNewGameConfirm = false; rt.mode = Mode::TitleHelp; break; + case TitleMenuAction::Quit: + rt.titleNewGameConfirm = false; + rt.exitRequested = true; + break; } } diff --git a/submission/report/report.md b/submission/report/report.md new file mode 100644 index 0000000..b630108 --- /dev/null +++ b/submission/report/report.md @@ -0,0 +1,72 @@ +# ManaPetWorld AI Agent 实验报告 + +## 1. 如何提示 Agent 实现需求 + +我采用了"ai 骑着 Agent 跑"的提示方式。每次让 Agent 修改前,先说明要实现的游戏体验,利用 LLM 先生成提示词和计划。 + +## 2. 如何让多轮修改的进度可重用 + +第一,要求 Agent 在每轮开始前先读取项目说明和现有代码模式。使用 `codegraph` MCP,辅助完成这项任务 + +第二,把需求拆成可验收的小任务。使用 `plan` 模式帮助设计。 + +第三,人工调试。人工验收并使用调试模式人工定位具体问题。 + +## 3. 多次尝试后仍然困难的错误及解决过程 + +实验中最难处理的问题是地图重定位和传送落点错误。 + +- TMX 地图中的传送对象坐标配置错误。 +- 世界地图 placement 与单张地图局部坐标换算不一致。 +- Warp 目标读取时把瓦片坐标和像素坐标混用。 +- 玩家重定位后朝向、碰撞或附近 Warp 状态没有同步更新。 +- 地图名使用了旧资源名或别名,导致目标地图解析不稳定。 + +Agent 前几次只根据代码推断问题,定位不够准确。之后我改变提示方式,不再直接要求“修好传送”,而是要求 Agent 先增加调试模式。 + +最终采用的解决方式是让 AI 增加 `F3` 坐标调试模式。调试面板显示: + +- 当前地图名 `Map` +- 玩家局部坐标 `Local` +- 世界坐标 `World` +- 当前世界坐标覆盖到的地图列表 +- 附近传送点及其目标地图 `Warp` + +有了这些信息后,我可以重新定位问题:先在错误现场按 `F3`,记录当前地图和坐标,再告诉 Agent 修改方向,例如“当前位置已经进入目标地图,但 World 坐标覆盖不对,优先检查 LocalToWorld 换算和 world placement”,或者“附近 Warp 目标名正确但落点偏移,优先检查 ReadWarpTarget 和 TMX 对象坐标单位”。这比单纯描述“传送错了”更有效。 + +## 4. 最终效果 + +- 支持 NPC 对话、野外宠物遭遇、战斗、背包、捕捉符和任务日志。 +- 支持小地图、日志面板、任务面板等 UI 展示。 + +

+ 开始界面 +

+ +

+ 图1:开始界面 +

+ +

+ 任务和地图探索 +

+ +

+ 图2:任务和地图探索 +

+ +

+ 战斗界面 +

+ +

+ 图3:战斗界面 +

+ +

+ 背包界面 +

+ +

+ 图4:背包界面 +

diff --git a/submission/screenshot/image0.png b/submission/screenshot/image0.png index a486fd1..fc34c25 100644 Binary files a/submission/screenshot/image0.png and b/submission/screenshot/image0.png differ diff --git a/submission/screenshot/image1.png b/submission/screenshot/image1.png index 970a22a..416d482 100644 Binary files a/submission/screenshot/image1.png and b/submission/screenshot/image1.png differ diff --git a/submission/screenshot/image2.png b/submission/screenshot/image2.png index 3cb9edf..eb2747a 100644 Binary files a/submission/screenshot/image2.png and b/submission/screenshot/image2.png differ diff --git a/submission/screenshot/image3.png b/submission/screenshot/image3.png index fdcb568..80e6e3d 100644 Binary files a/submission/screenshot/image3.png and b/submission/screenshot/image3.png differ diff --git a/tests/app/BattleEncounterTest.cpp b/tests/app/BattleEncounterTest.cpp new file mode 100644 index 0000000..09785e6 --- /dev/null +++ b/tests/app/BattleEncounterTest.cpp @@ -0,0 +1,47 @@ +#define main mana_pet_world_real_main +#include "../../src/app/main.cpp" +#undef main + +#include +#include +#include + +namespace { + +void StartBattleHidesTriggeredMonster() +{ + const std::filesystem::path savePath = std::filesystem::temp_directory_path() / "mana_pet_world_battle_encounter_test_save.txt"; + std::filesystem::remove(savePath); + + Runtime rt; + rt.currentMapName = "TestMap"; + rt.savePath = savePath; + + Entity wild; + wild.name = "TestMonster"; + wild.objectKey = "TestMap#monster#1"; + wild.pet = MakePet("Lulea", 20, 7); + wild.active = true; + wild.moving = true; + wild.respawnDelay = 42.0f; + rt.wildPets.push_back(wild); + + StartBattle(rt, 0); + + assert(rt.mode == Mode::Battle); + assert(rt.battlePetIndex == 0); + assert(!rt.wildPets[0].active); + assert(!rt.wildPets[0].moving); + assert(std::fabs(rt.wildPets[0].respawnTimer - wild.respawnDelay) < 0.001f); + assert(rt.monsterRespawnUntil.find(wild.objectKey) != rt.monsterRespawnUntil.end()); + + std::filesystem::remove(savePath); +} + +} // namespace + +int main() +{ + StartBattleHidesTriggeredMonster(); + return 0; +} diff --git a/tests/app/TitleMenuTest.cpp b/tests/app/TitleMenuTest.cpp new file mode 100644 index 0000000..ffc386d --- /dev/null +++ b/tests/app/TitleMenuTest.cpp @@ -0,0 +1,41 @@ +#include "TitleMenu.h" + +#include + +using mana::app::BuildTitleMenuItems; +using mana::app::TitleMenuAction; + +namespace { + +void MenuWithoutSaveOffersNewGameHelpAndQuit() +{ + const auto items = BuildTitleMenuItems(false); + + assert(items.size() == 3); + assert(items[0].action == TitleMenuAction::NewGame); + assert(items[0].label == "开始新游戏"); + assert(items[1].action == TitleMenuAction::Help); + assert(items[1].label == "帮助"); + assert(items[2].action == TitleMenuAction::Quit); + assert(items[2].label == "退出游戏"); +} + +void MenuWithSaveKeepsContinueFirstAndQuitLast() +{ + const auto items = BuildTitleMenuItems(true); + + assert(items.size() == 4); + assert(items.front().action == TitleMenuAction::ContinueGame); + assert(items.front().label == "继续游戏"); + assert(items.back().action == TitleMenuAction::Quit); + assert(items.back().label == "退出游戏"); +} + +} // namespace + +int main() +{ + MenuWithoutSaveOffersNewGameHelpAndQuit(); + MenuWithSaveKeepsContinueFirstAndQuitLast(); + return 0; +}