书写实验报告 调整开始界面 修复逃跑后重新进入战斗的问题
@@ -61,6 +61,7 @@ target_compile_options(mana_core PRIVATE -Wall -Wextra -Wpedantic)
|
|||||||
|
|
||||||
add_executable(mana_pet_world
|
add_executable(mana_pet_world
|
||||||
src/app/LogicalViewport.cpp
|
src/app/LogicalViewport.cpp
|
||||||
|
src/app/TitleMenu.cpp
|
||||||
src/app/main.cpp
|
src/app/main.cpp
|
||||||
src/battle/BattleScene.cpp
|
src/battle/BattleScene.cpp
|
||||||
)
|
)
|
||||||
@@ -69,10 +70,35 @@ target_compile_options(mana_pet_world PRIVATE -Wall -Wextra -Wpedantic)
|
|||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
add_executable(logical_viewport_test
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/LogicalViewportTest.cpp")
|
||||||
tests/app/LogicalViewportTest.cpp
|
add_executable(logical_viewport_test
|
||||||
src/app/LogicalViewport.cpp
|
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)
|
target_include_directories(logical_viewport_test PRIVATE src/app)
|
||||||
add_test(NAME logical_viewport_test COMMAND logical_viewport_test)
|
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()
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include "TitleMenu.h"
|
||||||
|
|
||||||
|
namespace mana::app {
|
||||||
|
|
||||||
|
std::vector<TitleMenuItem> BuildTitleMenuItems(bool hasSave)
|
||||||
|
{
|
||||||
|
std::vector<TitleMenuItem> 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
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace mana::app {
|
||||||
|
|
||||||
|
enum class TitleMenuAction {
|
||||||
|
ContinueGame,
|
||||||
|
NewGame,
|
||||||
|
Help,
|
||||||
|
Quit
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TitleMenuItem {
|
||||||
|
TitleMenuAction action = TitleMenuAction::ContinueGame;
|
||||||
|
std::string label;
|
||||||
|
bool enabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TitleMenuItem> BuildTitleMenuItems(bool hasSave);
|
||||||
|
|
||||||
|
} // namespace mana::app
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "ScriptedInteractable.h"
|
#include "ScriptedInteractable.h"
|
||||||
#include "SoundAssets.h"
|
#include "SoundAssets.h"
|
||||||
#include "SpriteAnimation.h"
|
#include "SpriteAnimation.h"
|
||||||
|
#include "TitleMenu.h"
|
||||||
#include "TmxMap.h"
|
#include "TmxMap.h"
|
||||||
#include "TmxMapPool.h"
|
#include "TmxMapPool.h"
|
||||||
#include "TmxWorld.h"
|
#include "TmxWorld.h"
|
||||||
@@ -55,6 +56,8 @@ using mana::app::kLogicalScreenHeight;
|
|||||||
using mana::app::kLogicalScreenWidth;
|
using mana::app::kLogicalScreenWidth;
|
||||||
using mana::app::kMinimumWindowHeight;
|
using mana::app::kMinimumWindowHeight;
|
||||||
using mana::app::kMinimumWindowWidth;
|
using mana::app::kMinimumWindowWidth;
|
||||||
|
using mana::app::TitleMenuAction;
|
||||||
|
using mana::app::TitleMenuItem;
|
||||||
|
|
||||||
Camera2D LogicalUiCamera(const mana::app::LogicalViewport& viewport)
|
Camera2D LogicalUiCamera(const mana::app::LogicalViewport& viewport)
|
||||||
{
|
{
|
||||||
@@ -184,12 +187,6 @@ enum class InventoryPage {
|
|||||||
Crafting
|
Crafting
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TitleMenuAction {
|
|
||||||
ContinueGame,
|
|
||||||
NewGame,
|
|
||||||
Help
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class PauseMenuAction {
|
enum class PauseMenuAction {
|
||||||
Resume,
|
Resume,
|
||||||
Save,
|
Save,
|
||||||
@@ -197,12 +194,6 @@ enum class PauseMenuAction {
|
|||||||
Quit
|
Quit
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TitleMenuItem {
|
|
||||||
TitleMenuAction action = TitleMenuAction::ContinueGame;
|
|
||||||
std::string label;
|
|
||||||
bool enabled = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PauseMenuItem {
|
struct PauseMenuItem {
|
||||||
PauseMenuAction action = PauseMenuAction::Resume;
|
PauseMenuAction action = PauseMenuAction::Resume;
|
||||||
std::string label;
|
std::string label;
|
||||||
@@ -2650,25 +2641,7 @@ std::optional<SaveState> LoadTitleSavePreview(const Runtime& rt)
|
|||||||
std::vector<TitleMenuItem> BuildTitleMenuItems(const Runtime& rt)
|
std::vector<TitleMenuItem> BuildTitleMenuItems(const Runtime& rt)
|
||||||
{
|
{
|
||||||
const bool hasSave = LoadTitleSavePreview(rt).has_value();
|
const bool hasSave = LoadTitleSavePreview(rt).has_value();
|
||||||
std::vector<TitleMenuItem> items;
|
return mana::app::BuildTitleMenuItems(hasSave);
|
||||||
if (hasSave) {
|
|
||||||
items.push_back({
|
|
||||||
TitleMenuAction::ContinueGame,
|
|
||||||
"继续游戏",
|
|
||||||
true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
items.push_back({
|
|
||||||
TitleMenuAction::NewGame,
|
|
||||||
"开始新游戏",
|
|
||||||
true,
|
|
||||||
});
|
|
||||||
items.push_back({
|
|
||||||
TitleMenuAction::Help,
|
|
||||||
"帮助",
|
|
||||||
true,
|
|
||||||
});
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DrawTitleButton(Runtime& rt, Font font, const TitleMenuItem& item, Rectangle bounds, bool selected)
|
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, "托诺里宠物世界", {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});
|
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<TitleMenuItem> items = BuildTitleMenuItems(rt);
|
||||||
|
rt.selectedTitleSlot = std::clamp(rt.selectedTitleSlot, 0, static_cast<int>(items.size()) - 1);
|
||||||
|
|
||||||
|
const float rowStartY = 330.0f;
|
||||||
|
const float rowSpacing = 78.0f;
|
||||||
|
const float rowHeight = 62.0f;
|
||||||
|
const float lastRowBottom = rowStartY + static_cast<float>(std::max(0, static_cast<int>(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);
|
DrawWindowPanel(rt, menuPanel);
|
||||||
DrawIcon(rt, "icon/map.png", {106.0f, 266.0f}, 30.0f);
|
DrawIcon(rt, "icon/map.png", {106.0f, 266.0f}, 30.0f);
|
||||||
DrawTextCn(font, "开始界面", {146.0f, 265.0f}, 27.0f, Color{255, 255, 221, 255});
|
DrawTextCn(font, "开始界面", {146.0f, 265.0f}, 27.0f, Color{255, 255, 221, 255});
|
||||||
|
|
||||||
std::vector<TitleMenuItem> items = BuildTitleMenuItems(rt);
|
|
||||||
rt.selectedTitleSlot = std::clamp(rt.selectedTitleSlot, 0, static_cast<int>(items.size()) - 1);
|
|
||||||
for (int i = 0; i < static_cast<int>(items.size()); ++i) {
|
for (int i = 0; i < static_cast<int>(items.size()); ++i) {
|
||||||
const Rectangle row{110.0f, 330.0f + static_cast<float>(i) * 78.0f, 420.0f, 62.0f};
|
const Rectangle row{110.0f, rowStartY + static_cast<float>(i) * rowSpacing, 420.0f, rowHeight};
|
||||||
DrawTitleButton(rt, font, items[static_cast<std::size_t>(i)], row, i == rt.selectedTitleSlot);
|
DrawTitleButton(rt, font, items[static_cast<std::size_t>(i)], row, i == rt.selectedTitleSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2742,7 +2720,7 @@ void DrawTitleMenu(Runtime& rt, Font font)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rt.titleNewGameConfirm) {
|
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);
|
RestoreActivePetForRetreat(rt);
|
||||||
rt.battle.player = rt.team.pets.empty() ? MakePet("Lulea", 30, 7) : rt.team.pets.front();
|
rt.battle.player = rt.team.pets.empty() ? MakePet("Lulea", 30, 7) : rt.team.pets.front();
|
||||||
rt.battle.wild = rt.wildPets[petIndex].pet;
|
rt.battle.wild = rt.wildPets[petIndex].pet;
|
||||||
|
ScheduleMonsterRespawn(rt, rt.wildPets[petIndex]);
|
||||||
RegisterPetSeen(rt.petJournal, rt.battle.wild.name);
|
RegisterPetSeen(rt.petJournal, rt.battle.wild.name);
|
||||||
ApplyQuestEvent(rt.questRuntime, QuestEvent::SawPet(rt.battle.wild.name));
|
ApplyQuestEvent(rt.questRuntime, QuestEvent::SawPet(rt.battle.wild.name));
|
||||||
rt.battle.message = "野外宠物出现了";
|
rt.battle.message = "野外宠物出现了";
|
||||||
@@ -5734,6 +5713,10 @@ void ExecuteTitleMenuAction(Runtime& rt, TitleMenuAction action, bool enabled)
|
|||||||
rt.titleNewGameConfirm = false;
|
rt.titleNewGameConfirm = false;
|
||||||
rt.mode = Mode::TitleHelp;
|
rt.mode = Mode::TitleHelp;
|
||||||
break;
|
break;
|
||||||
|
case TitleMenuAction::Quit:
|
||||||
|
rt.titleNewGameConfirm = false;
|
||||||
|
rt.exitRequested = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 展示。
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="../screenshot/image0.png" alt="开始界面" width="600">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
图1:开始界面
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="../screenshot/image1.png" alt="任务和地图探索" width="600">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
图2:任务和地图探索
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="../screenshot/image2.png" alt="战斗界面" width="600">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
图3:战斗界面
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="../screenshot/image3.png" alt="背包界面" width="600">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
图4:背包界面
|
||||||
|
</p>
|
||||||
|
Before Width: | Height: | Size: 648 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 774 KiB After Width: | Height: | Size: 755 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 712 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
#define main mana_pet_world_real_main
|
||||||
|
#include "../../src/app/main.cpp"
|
||||||
|
#undef main
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#include "TitleMenu.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||