From f6291b2aaceb7ff459d0f0169f54e650f67f51d3 Mon Sep 17 00:00:00 2001 From: FanstyNight Date: Wed, 3 Jun 2026 23:21:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A0=E7=89=A9=E4=BC=A4?= =?UTF-8?q?=E5=AE=B3=E5=BC=82=E5=B8=B8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 18 ++++ src/app/main.cpp | 187 +++++++++++++++++++++++++++++++++++------- src/core/GameCore.cpp | 18 +++- src/core/GameCore.h | 1 + 4 files changed, 192 insertions(+), 32 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b53026..887be65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,3 +107,21 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/BattleEncounterTest.cpp") target_compile_options(battle_encounter_test PRIVATE -Wall -Wextra -Wpedantic) add_test(NAME battle_encounter_test COMMAND battle_encounter_test) endif() + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/CapturePetTest.cpp") + add_executable(capture_pet_test + tests/core/CapturePetTest.cpp + ) + target_link_libraries(capture_pet_test PRIVATE mana_core) + target_compile_options(capture_pet_test PRIVATE -Wall -Wextra -Wpedantic) + add_test(NAME capture_pet_test COMMAND capture_pet_test) +endif() + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/core/TeamPetRulesTest.cpp") + add_executable(team_pet_rules_test + tests/core/TeamPetRulesTest.cpp + ) + target_link_libraries(team_pet_rules_test PRIVATE mana_core) + target_compile_options(team_pet_rules_test PRIVATE -Wall -Wextra -Wpedantic) + add_test(NAME team_pet_rules_test COMMAND team_pet_rules_test) +endif() diff --git a/src/app/main.cpp b/src/app/main.cpp index 5339246..7808fdf 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -169,6 +169,7 @@ enum class Mode { Pause, Dialogue, StarterChoice, + StarterHealService, MerchantGreeting, Battle, Shop, @@ -257,6 +258,7 @@ struct Runtime { int selectedMapIndex = 0; int selectedInventorySlot = 0; int selectedStarterSlot = 0; + int selectedStarterServiceSlot = 0; int selectedQuestSlot = 0; int selectedShopSlot = 0; int selectedPetDexSlot = 0; @@ -309,6 +311,7 @@ struct Runtime { bool SaveRuntime(Runtime& rt); std::string VisibleNpcName(const std::string& npcName); +void PlayRuntimeSound(Runtime& rt, const std::string& purpose); struct StarterPetOption { std::string speciesName; @@ -325,6 +328,8 @@ const std::vector& StarterPetOptions() return options; } +constexpr int kStarterTeamRestoreCost = 120; + std::filesystem::path DetectRoot(int argc, char** argv) { for (int i = 1; i < argc; ++i) { @@ -450,11 +455,20 @@ void BindStarterNpc(Runtime& rt) } } +bool IsStarterNpcIdentity(const Runtime& rt, const Entity& npc) +{ + return !rt.starterNpcName.empty() + && npc.name == rt.starterNpcName; +} + bool IsStarterNpc(const Runtime& rt, const Entity& npc) { - return !IsStarterChoiceComplete(rt) - && !rt.starterNpcName.empty() - && npc.name == rt.starterNpcName; + return !IsStarterChoiceComplete(rt) && IsStarterNpcIdentity(rt, npc); +} + +bool ShouldOfferStarterHealService(const Runtime& rt, const Entity& npc) +{ + return IsStarterChoiceComplete(rt) && IsStarterNpcIdentity(rt, npc); } bool ShouldOpenStarterChoiceAfterFunction(const Runtime& rt, const Entity& npc, const std::string& functionName) @@ -469,6 +483,13 @@ void OpenStarterChoice(Runtime& rt, const Entity& npc) rt.message = VisibleNpcName(npc.name) + "让你选择初始宠物"; } +void OpenStarterHealService(Runtime& rt, const Entity& npc) +{ + rt.mode = Mode::StarterHealService; + rt.selectedStarterServiceSlot = 0; + rt.message = VisibleNpcName(npc.name) + "可以恢复你的出战队伍"; +} + void AwardStarterPet(Runtime& rt, const std::string& speciesName) { const EntityPreset starter = LoadEntityPreset(rt.root, speciesName); @@ -491,15 +512,6 @@ const QuestObjectiveProgress* FirstIncompleteObjective(const QuestRuntime& quest return objective == questRuntime.objectives.end() ? nullptr : &*objective; } -void RestoreActivePetForRetreat(Runtime& rt) -{ - if (rt.team.pets.empty()) { - return; - } - Pet& active = rt.team.pets.front(); - active.hp = std::max(active.hp, std::max(1, active.maxHp / 2)); -} - std::filesystem::path GuiTexture(const std::filesystem::path& root, const std::string& relative) { return root / "assets/ui" / relative; @@ -4094,6 +4106,36 @@ void DrawMerchantGreeting(Runtime& rt, Font font) DrawDialogueButton(rt, font, "继续", {1102.0f, 596.0f}); } +void DrawStarterHealService(Runtime& rt, Font font) +{ + DrawDialogueBox(rt, {96.0f, 498.0f, 1088.0f, 166.0f}); + const std::string title = rt.nearbyNpc ? VisibleNpcName(rt.nearbyNpc->name) : "队伍恢复"; + DrawTextCn(font, title.c_str(), {142.0f, 520.0f}, 25.0f, Color{255, 255, 221, 255}); + DrawWrappedText( + font, + "巡逻队可以帮你的出战队伍恢复体力。体力为 0 的宠物恢复前不能设为首发。", + {142.0f, 552.0f}, + 20.0f, + 850.0f, + Color{238, 216, 161, 255}); + const std::string options[] = { + "恢复出战队伍 " + std::to_string(kStarterTeamRestoreCost) + " 金币", + "继续对话", + }; + for (int i = 0; i < 2; ++i) { + const bool selected = i == rt.selectedStarterServiceSlot; + const std::string marker = selected ? "> " : " "; + DrawTextCn( + font, + (marker + std::to_string(i + 1) + ". " + options[i]).c_str(), + {166.0f, 594.0f + static_cast(i) * 22.0f}, + 18.0f, + selected ? Color{255, 255, 221, 255} : Color{238, 216, 161, 255}); + } + DrawTextCn(font, ("金币 " + std::to_string(rt.gold)).c_str(), {842.0f, 520.0f}, 18.0f, Color{193, 151, 71, 255}); + DrawTextCn(font, "上下键切换 确认键选择 关闭键返回", {730.0f, 640.0f}, 17.0f, Color{193, 151, 71, 255}); +} + void UpdateStarterChoice(Runtime& rt) { const int count = static_cast(StarterPetOptions().size()); @@ -4128,6 +4170,79 @@ void UpdateStarterChoice(Runtime& rt) } } +int EnterDialogueIndex(Runtime& rt, Entity& npc, int entryIndex, int fallbackIndex); + +void StartNpcDialogue(Runtime& rt, Entity& npc) +{ + std::string startFunction = ResolveDialogueStartFunction( + npc.dialogue, rt.questRuntime.states.RawStates(), "OnStart"); + if (npc.playerScript.ends_with("Kael.gd")) { + if (ShouldBlockKaelPetTrialStart(startFunction, rt.team)) { + rt.message = "凯尔 请先捕捉一只野外宠物加入队伍 再回来汇报"; + return; + } + startFunction = ResolveKaelPetTrialStart(startFunction, rt.team); + } + ApplyQuestEvent(rt.questRuntime, QuestEvent::TalkedToNpc(npc.name)); + rt.mode = Mode::Dialogue; + rt.dialogueCloseOnMenuReturn = false; + auto start = npc.dialogue.functionStart.find(startFunction); + if (start == npc.dialogue.functionStart.end()) { + start = npc.dialogue.functionStart.find("OnStart"); + } + rt.dialogueIndex = start != npc.dialogue.functionStart.end() ? start->second : 0; + rt.dialogueIndex = EnterDialogueIndex(rt, npc, rt.dialogueIndex, rt.dialogueIndex); +} + +void UpdateStarterHealService(Runtime& rt) +{ + if (IsKeyPressed(KEY_ESCAPE) || IsKeyPressed(KEY_B)) { + rt.mode = Mode::Explore; + return; + } + if (IsKeyPressed(KEY_UP) || IsKeyPressed(KEY_W) || IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_S)) { + rt.selectedStarterServiceSlot = 1 - std::clamp(rt.selectedStarterServiceSlot, 0, 1); + } + if (IsKeyPressed(KEY_ONE)) { + rt.selectedStarterServiceSlot = 0; + } + if (IsKeyPressed(KEY_TWO)) { + rt.selectedStarterServiceSlot = 1; + } + if (!IsKeyPressed(KEY_ENTER) && !IsKeyPressed(KEY_SPACE) && !IsKeyPressed(KEY_E) + && !IsKeyPressed(KEY_ONE) && !IsKeyPressed(KEY_TWO)) { + return; + } + rt.selectedStarterServiceSlot = std::clamp(rt.selectedStarterServiceSlot, 0, 1); + if (rt.selectedStarterServiceSlot == 1) { + if (rt.nearbyNpc) { + StartNpcDialogue(rt, *rt.nearbyNpc); + } else { + rt.mode = Mode::Explore; + } + return; + } + if (rt.team.pets.empty()) { + rt.message = "没有可恢复的出战宠物"; + PlayRuntimeSound(rt, "hit"); + return; + } + if (rt.gold < kStarterTeamRestoreCost) { + rt.message = "金币不足 需要 " + std::to_string(kStarterTeamRestoreCost); + PlayRuntimeSound(rt, "hit"); + return; + } + if (!RestoreTeamToFull(rt.team)) { + rt.message = "出战队伍体力已经满了"; + PlayRuntimeSound(rt, "hit"); + return; + } + rt.gold -= kStarterTeamRestoreCost; + rt.message = "出战队伍已恢复 消耗 " + std::to_string(kStarterTeamRestoreCost) + " 金币"; + PlayRuntimeSound(rt, "heal"); + SaveRuntime(rt); +} + void DrawWarpConfirm(Runtime& rt, Font font) { DrawDialogueBox(rt, {96.0f, 498.0f, 1088.0f, 166.0f}); @@ -4462,7 +4577,6 @@ void StartBattle(Runtime& rt, int petIndex, bool playerFirstAction = false) rt.battleMenuIndex = 0; rt.battlePlayerFirstAction = playerFirstAction; rt.battle = {}; - 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]); @@ -4930,6 +5044,11 @@ bool TryStartPetBattleThrow(Runtime& rt) rt.message = "没有可投出的首发宠物"; return false; } + if (rt.team.pets.front().hp <= 0) { + rt.message = PetDisplayName(rt.team.pets.front().name) + " 体力为0 请先找凯尔恢复"; + PlayRuntimeSound(rt, "hit"); + return false; + } const int targetIndex = FindCaptureThrowTarget(rt); if (targetIndex < 0) { return false; @@ -6233,24 +6352,11 @@ void UpdateExplore(Runtime& rt, float dt) rt.message = TonoriMerchantName(merchant) + "正在招呼你"; return; } - std::string startFunction = ResolveDialogueStartFunction( - rt.nearbyNpc->dialogue, rt.questRuntime.states.RawStates(), "OnStart"); - if (rt.nearbyNpc->playerScript.ends_with("Kael.gd")) { - if (ShouldBlockKaelPetTrialStart(startFunction, rt.team)) { - rt.message = "凯尔 请先捕捉一只野外宠物加入队伍 再回来汇报"; - return; - } - startFunction = ResolveKaelPetTrialStart(startFunction, rt.team); + if (ShouldOfferStarterHealService(rt, *rt.nearbyNpc)) { + OpenStarterHealService(rt, *rt.nearbyNpc); + return; } - ApplyQuestEvent(rt.questRuntime, QuestEvent::TalkedToNpc(rt.nearbyNpc->name)); - rt.mode = Mode::Dialogue; - rt.dialogueCloseOnMenuReturn = false; - auto start = rt.nearbyNpc->dialogue.functionStart.find(startFunction); - if (start == rt.nearbyNpc->dialogue.functionStart.end()) { - start = rt.nearbyNpc->dialogue.functionStart.find("OnStart"); - } - rt.dialogueIndex = start != rt.nearbyNpc->dialogue.functionStart.end() ? start->second : 0; - rt.dialogueIndex = EnterDialogueIndex(rt, *rt.nearbyNpc, rt.dialogueIndex, rt.dialogueIndex); + StartNpcDialogue(rt, *rt.nearbyNpc); return; } @@ -6288,6 +6394,11 @@ void UpdateExplore(Runtime& rt, float dt) rt.message = "先找" + ChineseTextOr(VisibleNpcName(rt.starterNpcName), "附近的训练员") + "选择初始宠物"; return; } + if (!rt.team.pets.empty() && rt.team.pets.front().hp <= 0) { + rt.message = PetDisplayName(rt.team.pets.front().name) + " 体力为0 请先找凯尔恢复"; + PlayRuntimeSound(rt, "hit"); + return; + } StartBattle(rt, i); return; } @@ -6835,6 +6946,12 @@ void UpdateInventory(Runtime& rt) } if (IsKeyPressed(KEY_ENTER)) { if (rt.inventoryPage == InventoryPage::Pets) { + const Pet& selectedPet = rt.team.pets[static_cast(rt.selectedInventorySlot)]; + if (selectedPet.hp <= 0) { + rt.message = PetDisplayName(selectedPet.name) + " 体力为0 不能设为首发"; + PlayRuntimeSound(rt, "hit"); + return; + } if (rt.selectedInventorySlot > 0) { std::rotate( rt.team.pets.begin(), @@ -6852,6 +6969,9 @@ void UpdateInventory(Runtime& rt) rt.message = PetDisplayName(species) + " 已加入出战队伍"; RefreshProgress(rt); SaveRuntime(rt); + } else { + rt.message = PetDisplayName(species) + " 体力为0 不能加入出战队伍"; + PlayRuntimeSound(rt, "hit"); } } } @@ -6968,6 +7088,9 @@ void UpdatePetDex(Runtime& rt) rt.message = PetDisplayName(selected.speciesName) + " 已设为出战宠物"; RefreshProgress(rt); SaveRuntime(rt); + } else if (selected.activeCount > 0 || selected.storageCount > 0) { + rt.message = PetDisplayName(selected.speciesName) + " 体力为0 不能设为出战宠物"; + PlayRuntimeSound(rt, "hit"); } else { rt.message = "还未拥有这只宠物"; } @@ -7073,6 +7196,8 @@ int main(int argc, char** argv) UpdateDialogue(rt); } else if (rt.mode == Mode::StarterChoice) { UpdateStarterChoice(rt); + } else if (rt.mode == Mode::StarterHealService) { + UpdateStarterHealService(rt); } else if (rt.mode == Mode::MerchantGreeting) { UpdateMerchantGreeting(rt); } else if (rt.mode == Mode::Battle) { @@ -7300,6 +7425,8 @@ int main(int argc, char** argv) DrawDialogue(rt, font); } else if (rt.mode == Mode::StarterChoice) { DrawStarterChoice(rt, font); + } else if (rt.mode == Mode::StarterHealService) { + DrawStarterHealService(rt, font); } else if (rt.mode == Mode::MerchantGreeting) { DrawMerchantGreeting(rt, font); } else if (rt.mode == Mode::Shop) { diff --git a/src/core/GameCore.cpp b/src/core/GameCore.cpp index c853e4d..28cab7a 100644 --- a/src/core/GameCore.cpp +++ b/src/core/GameCore.cpp @@ -471,7 +471,7 @@ bool AddPet(Team& team, const Pet& pet) bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName) { const auto found = std::find_if(team.pets.begin(), team.pets.end(), [&](const Pet& pet) { - return pet.name == speciesName; + return pet.name == speciesName && pet.hp > 0; }); if (found == team.pets.end()) { return false; @@ -483,7 +483,7 @@ bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName) bool ActivateStoredPet(Team& team, const std::string& speciesName) { const auto found = std::find_if(team.storage.begin(), team.storage.end(), [&](const Pet& pet) { - return pet.name == speciesName; + return pet.name == speciesName && pet.hp > 0; }); if (found == team.storage.end()) { return false; @@ -501,6 +501,19 @@ bool ActivateStoredPet(Team& team, const std::string& speciesName) return true; } +bool RestoreTeamToFull(Team& team) +{ + bool changed = false; + for (Pet& pet : team.pets) { + const int fullHp = std::max(1, pet.maxHp); + if (pet.hp != fullHp) { + pet.hp = fullHp; + changed = true; + } + } + return changed; +} + PetCollectionSummary BuildPetCollectionSummary(const Team& team) { std::set uniqueSpecies; @@ -662,6 +675,7 @@ CaptureResult TryCapture(BattleState& battle, Team& team, float capturePower, fl if (capturePower >= 1.0f - chance) { const bool activeTeamFull = team.pets.size() >= Team::MaxPets; Pet captured = battle.wild; + NormalizePetAfterLoad(captured); captured.hp = captured.maxHp; AddPet(team, captured); battle.finished = true; diff --git a/src/core/GameCore.h b/src/core/GameCore.h index b862e9f..5c80ff4 100644 --- a/src/core/GameCore.h +++ b/src/core/GameCore.h @@ -131,6 +131,7 @@ float CatchChance(const Pet& target); bool AddPet(Team& team, const Pet& pet); bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName); bool ActivateStoredPet(Team& team, const std::string& speciesName); +bool RestoreTeamToFull(Team& team); PetCollectionSummary BuildPetCollectionSummary(const Team& team); void RegisterPetSeen(PetJournal& journal, const std::string& speciesName); void RegisterPetCaught(PetJournal& journal, const std::string& speciesName);