修复宠物伤害异常问题

This commit is contained in:
2026-06-03 23:21:02 +08:00
parent 93ba207b74
commit f6291b2aac
4 changed files with 192 additions and 32 deletions
+18
View File
@@ -107,3 +107,21 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tests/app/BattleEncounterTest.cpp")
target_compile_options(battle_encounter_test PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(battle_encounter_test PRIVATE -Wall -Wextra -Wpedantic)
add_test(NAME battle_encounter_test COMMAND battle_encounter_test) add_test(NAME battle_encounter_test COMMAND battle_encounter_test)
endif() 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()
+157 -30
View File
@@ -169,6 +169,7 @@ enum class Mode {
Pause, Pause,
Dialogue, Dialogue,
StarterChoice, StarterChoice,
StarterHealService,
MerchantGreeting, MerchantGreeting,
Battle, Battle,
Shop, Shop,
@@ -257,6 +258,7 @@ struct Runtime {
int selectedMapIndex = 0; int selectedMapIndex = 0;
int selectedInventorySlot = 0; int selectedInventorySlot = 0;
int selectedStarterSlot = 0; int selectedStarterSlot = 0;
int selectedStarterServiceSlot = 0;
int selectedQuestSlot = 0; int selectedQuestSlot = 0;
int selectedShopSlot = 0; int selectedShopSlot = 0;
int selectedPetDexSlot = 0; int selectedPetDexSlot = 0;
@@ -309,6 +311,7 @@ struct Runtime {
bool SaveRuntime(Runtime& rt); bool SaveRuntime(Runtime& rt);
std::string VisibleNpcName(const std::string& npcName); std::string VisibleNpcName(const std::string& npcName);
void PlayRuntimeSound(Runtime& rt, const std::string& purpose);
struct StarterPetOption { struct StarterPetOption {
std::string speciesName; std::string speciesName;
@@ -325,6 +328,8 @@ const std::vector<StarterPetOption>& StarterPetOptions()
return options; return options;
} }
constexpr int kStarterTeamRestoreCost = 120;
std::filesystem::path DetectRoot(int argc, char** argv) std::filesystem::path DetectRoot(int argc, char** argv)
{ {
for (int i = 1; i < argc; ++i) { 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) bool IsStarterNpc(const Runtime& rt, const Entity& npc)
{ {
return !IsStarterChoiceComplete(rt) return !IsStarterChoiceComplete(rt) && IsStarterNpcIdentity(rt, npc);
&& !rt.starterNpcName.empty() }
&& npc.name == rt.starterNpcName;
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) 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) + "让你选择初始宠物"; 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) void AwardStarterPet(Runtime& rt, const std::string& speciesName)
{ {
const EntityPreset starter = LoadEntityPreset(rt.root, 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; 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) std::filesystem::path GuiTexture(const std::filesystem::path& root, const std::string& relative)
{ {
return root / "assets/ui" / relative; return root / "assets/ui" / relative;
@@ -4094,6 +4106,36 @@ void DrawMerchantGreeting(Runtime& rt, Font font)
DrawDialogueButton(rt, font, "继续", {1102.0f, 596.0f}); 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<float>(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) void UpdateStarterChoice(Runtime& rt)
{ {
const int count = static_cast<int>(StarterPetOptions().size()); const int count = static_cast<int>(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) void DrawWarpConfirm(Runtime& rt, Font font)
{ {
DrawDialogueBox(rt, {96.0f, 498.0f, 1088.0f, 166.0f}); 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.battleMenuIndex = 0;
rt.battlePlayerFirstAction = playerFirstAction; rt.battlePlayerFirstAction = playerFirstAction;
rt.battle = {}; rt.battle = {};
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]); ScheduleMonsterRespawn(rt, rt.wildPets[petIndex]);
@@ -4930,6 +5044,11 @@ bool TryStartPetBattleThrow(Runtime& rt)
rt.message = "没有可投出的首发宠物"; rt.message = "没有可投出的首发宠物";
return false; 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); const int targetIndex = FindCaptureThrowTarget(rt);
if (targetIndex < 0) { if (targetIndex < 0) {
return false; return false;
@@ -6233,24 +6352,11 @@ void UpdateExplore(Runtime& rt, float dt)
rt.message = TonoriMerchantName(merchant) + "正在招呼你"; rt.message = TonoriMerchantName(merchant) + "正在招呼你";
return; return;
} }
std::string startFunction = ResolveDialogueStartFunction( if (ShouldOfferStarterHealService(rt, *rt.nearbyNpc)) {
rt.nearbyNpc->dialogue, rt.questRuntime.states.RawStates(), "OnStart"); OpenStarterHealService(rt, *rt.nearbyNpc);
if (rt.nearbyNpc->playerScript.ends_with("Kael.gd")) { return;
if (ShouldBlockKaelPetTrialStart(startFunction, rt.team)) {
rt.message = "凯尔 请先捕捉一只野外宠物加入队伍 再回来汇报";
return;
}
startFunction = ResolveKaelPetTrialStart(startFunction, rt.team);
} }
ApplyQuestEvent(rt.questRuntime, QuestEvent::TalkedToNpc(rt.nearbyNpc->name)); StartNpcDialogue(rt, *rt.nearbyNpc);
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);
return; return;
} }
@@ -6288,6 +6394,11 @@ void UpdateExplore(Runtime& rt, float dt)
rt.message = "先找" + ChineseTextOr(VisibleNpcName(rt.starterNpcName), "附近的训练员") + "选择初始宠物"; rt.message = "先找" + ChineseTextOr(VisibleNpcName(rt.starterNpcName), "附近的训练员") + "选择初始宠物";
return; 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); StartBattle(rt, i);
return; return;
} }
@@ -6835,6 +6946,12 @@ void UpdateInventory(Runtime& rt)
} }
if (IsKeyPressed(KEY_ENTER)) { if (IsKeyPressed(KEY_ENTER)) {
if (rt.inventoryPage == InventoryPage::Pets) { if (rt.inventoryPage == InventoryPage::Pets) {
const Pet& selectedPet = rt.team.pets[static_cast<std::size_t>(rt.selectedInventorySlot)];
if (selectedPet.hp <= 0) {
rt.message = PetDisplayName(selectedPet.name) + " 体力为0 不能设为首发";
PlayRuntimeSound(rt, "hit");
return;
}
if (rt.selectedInventorySlot > 0) { if (rt.selectedInventorySlot > 0) {
std::rotate( std::rotate(
rt.team.pets.begin(), rt.team.pets.begin(),
@@ -6852,6 +6969,9 @@ void UpdateInventory(Runtime& rt)
rt.message = PetDisplayName(species) + " 已加入出战队伍"; rt.message = PetDisplayName(species) + " 已加入出战队伍";
RefreshProgress(rt); RefreshProgress(rt);
SaveRuntime(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) + " 已设为出战宠物"; rt.message = PetDisplayName(selected.speciesName) + " 已设为出战宠物";
RefreshProgress(rt); RefreshProgress(rt);
SaveRuntime(rt); SaveRuntime(rt);
} else if (selected.activeCount > 0 || selected.storageCount > 0) {
rt.message = PetDisplayName(selected.speciesName) + " 体力为0 不能设为出战宠物";
PlayRuntimeSound(rt, "hit");
} else { } else {
rt.message = "还未拥有这只宠物"; rt.message = "还未拥有这只宠物";
} }
@@ -7073,6 +7196,8 @@ int main(int argc, char** argv)
UpdateDialogue(rt); UpdateDialogue(rt);
} else if (rt.mode == Mode::StarterChoice) { } else if (rt.mode == Mode::StarterChoice) {
UpdateStarterChoice(rt); UpdateStarterChoice(rt);
} else if (rt.mode == Mode::StarterHealService) {
UpdateStarterHealService(rt);
} else if (rt.mode == Mode::MerchantGreeting) { } else if (rt.mode == Mode::MerchantGreeting) {
UpdateMerchantGreeting(rt); UpdateMerchantGreeting(rt);
} else if (rt.mode == Mode::Battle) { } else if (rt.mode == Mode::Battle) {
@@ -7300,6 +7425,8 @@ int main(int argc, char** argv)
DrawDialogue(rt, font); DrawDialogue(rt, font);
} else if (rt.mode == Mode::StarterChoice) { } else if (rt.mode == Mode::StarterChoice) {
DrawStarterChoice(rt, font); DrawStarterChoice(rt, font);
} else if (rt.mode == Mode::StarterHealService) {
DrawStarterHealService(rt, font);
} else if (rt.mode == Mode::MerchantGreeting) { } else if (rt.mode == Mode::MerchantGreeting) {
DrawMerchantGreeting(rt, font); DrawMerchantGreeting(rt, font);
} else if (rt.mode == Mode::Shop) { } else if (rt.mode == Mode::Shop) {
+16 -2
View File
@@ -471,7 +471,7 @@ bool AddPet(Team& team, const Pet& pet)
bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName) bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName)
{ {
const auto found = std::find_if(team.pets.begin(), team.pets.end(), [&](const Pet& pet) { 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()) { if (found == team.pets.end()) {
return false; return false;
@@ -483,7 +483,7 @@ bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName)
bool ActivateStoredPet(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) { 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()) { if (found == team.storage.end()) {
return false; return false;
@@ -501,6 +501,19 @@ bool ActivateStoredPet(Team& team, const std::string& speciesName)
return true; 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) PetCollectionSummary BuildPetCollectionSummary(const Team& team)
{ {
std::set<std::string> uniqueSpecies; std::set<std::string> uniqueSpecies;
@@ -662,6 +675,7 @@ CaptureResult TryCapture(BattleState& battle, Team& team, float capturePower, fl
if (capturePower >= 1.0f - chance) { if (capturePower >= 1.0f - chance) {
const bool activeTeamFull = team.pets.size() >= Team::MaxPets; const bool activeTeamFull = team.pets.size() >= Team::MaxPets;
Pet captured = battle.wild; Pet captured = battle.wild;
NormalizePetAfterLoad(captured);
captured.hp = captured.maxHp; captured.hp = captured.maxHp;
AddPet(team, captured); AddPet(team, captured);
battle.finished = true; battle.finished = true;
+1
View File
@@ -131,6 +131,7 @@ float CatchChance(const Pet& target);
bool AddPet(Team& team, const Pet& pet); bool AddPet(Team& team, const Pet& pet);
bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName); bool MoveFirstTeamPetToFront(Team& team, const std::string& speciesName);
bool ActivateStoredPet(Team& team, const std::string& speciesName); bool ActivateStoredPet(Team& team, const std::string& speciesName);
bool RestoreTeamToFull(Team& team);
PetCollectionSummary BuildPetCollectionSummary(const Team& team); PetCollectionSummary BuildPetCollectionSummary(const Team& team);
void RegisterPetSeen(PetJournal& journal, const std::string& speciesName); void RegisterPetSeen(PetJournal& journal, const std::string& speciesName);
void RegisterPetCaught(PetJournal& journal, const std::string& speciesName); void RegisterPetCaught(PetJournal& journal, const std::string& speciesName);