diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 107afc1ed3..8f6a40ae13 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4564,6 +4564,7 @@ public: cXyz mIBChainInterpCurrHandRoot; bool mIBChainInterpPrevValid; bool mIBChainInterpCurrValid; + bool mIsRollstab = false; #endif }; // Size: 0x385C diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h index 5c2758b6b7..c93ea5d626 100644 --- a/include/dusk/achievements.h +++ b/include/dusk/achievements.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include "nlohmann/json.hpp" @@ -47,6 +47,7 @@ public: // Signals are visible to all achievement checks within the same tick, then cleared. void signal(const char* key); bool hasSignal(const char* key) const; + int signalCount(const char* key) const; std::vector getAchievements() const; @@ -62,7 +63,7 @@ private: void processEntry(Entry& e); std::vector m_entries; - std::unordered_set m_signals; + std::unordered_map m_signals; bool m_loaded = false; bool m_dirty = false; }; diff --git a/src/d/actor/d_a_alink_cut.inc b/src/d/actor/d_a_alink_cut.inc index f33d2935c5..ba3c0ba89e 100644 --- a/src/d/actor/d_a_alink_cut.inc +++ b/src/d/actor/d_a_alink_cut.inc @@ -1165,6 +1165,10 @@ int daAlink_c::procCutFinishInit(int i_type) { const daAlink_cutParamTbl* cutParams = &cutParamTable[i_type]; BOOL is_proc_frontRoll = mProcID == PROC_FRONT_ROLL; +#if TARGET_PC + mIsRollstab = (is_proc_frontRoll != FALSE) && (i_type == CUT_FINISH_PARAM_STAB); +#endif + commonProcInit(PROC_CUT_FINISH); setCutType(cutParams->m_cutType); field_0x3198 = cutParams->m_recoilAnmID; diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index d96e6314af..0f982e59e8 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -2192,6 +2192,9 @@ static void damage_check(b_gnd_class* i_this) { i_this->mDamageInvulnerabilityTimer = 100; } } + #if TARGET_PC + dusk::AchievementSystem::get().signal("ganondorf_hit"); + #endif } cXyz hitmark_size(1.0f, 1.0f, 1.0f); @@ -2218,6 +2221,7 @@ static void damage_check(b_gnd_class* i_this) { i_this->field_0xc7c = 0; dScnPly_c::setPauseTimer(7); a_this->health = 100; + dusk::AchievementSystem::get().signal("ganondorf_knocked_down"); } break; } diff --git a/src/d/d_cc_uty.cpp b/src/d/d_cc_uty.cpp index 218cb362f4..91fc6677c1 100644 --- a/src/d/d_cc_uty.cpp +++ b/src/d/d_cc_uty.cpp @@ -16,6 +16,7 @@ #if TARGET_PC #include "dusk/achievements.h" #include "dusk/settings.h" +#include "d/actor/d_a_alink.h" #endif static int plCutLRC[58] = { @@ -448,6 +449,10 @@ fopAc_ac_c* cc_at_check(fopAc_ac_c* i_enemy, dCcU_AtInfo* i_AtInfo) { #if TARGET_PC if (fopAcM_GetGroup(i_enemy) == fopAc_ENEMY_e) { dusk::AchievementSystem::get().signal("enemy_killed"); + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link != nullptr && link->mProcID == daAlink_c::PROC_CUT_FINISH && link->mIsRollstab) { + dusk::AchievementSystem::get().signal("rollstab_kill"); + } } #endif } diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 5b76d7e8e8..d06ed404cc 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -11,6 +11,7 @@ #include "d/actor/d_a_alink.h" #include "d/actor/d_a_ni.h" #include "d/actor/d_a_npc4.h" +#include "d/actor/d_a_b_gnd.h" #include "d/actor/d_a_b_ob.h" #include "d/actor/d_a_player.h" #include "d/d_demo.h" @@ -18,6 +19,7 @@ #include "f_pc/f_pc_name.h" #include "f_op/f_op_actor_mng.h" #include "f_pc/f_pc_name.h" +#include "dusk/logging.h" #include #include @@ -35,6 +37,9 @@ static void* s_cucco_play_search(void* i_actor, void*) { } static void checkGoatHerding(Achievement& a, int32_t threshMs) { + if (strcmp(dComIfGp_getStartStageName(), "F_SP00") != 0) { + return; + } if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) { return; } @@ -65,6 +70,25 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "three_heart_clear", + "Hero Mode", + "Defeat Ganondorf with only 3 heart containers.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + if (dComIfGs_getMaxLife() < 20) { + a.progress = 1; + } + }, + {} + }, { { "completionist", @@ -201,7 +225,7 @@ std::vector AchievementSystem::makeEntries() { hasAncientDoc = true; } } - if (!hasJewelRod || !hasAncientDoc) { + if (!hasJewelRod || (!hasAncientDoc && !dComIfGs_isEventBit(dSv_event_flag_c::F_0302))) { return; } @@ -265,7 +289,7 @@ std::vector AchievementSystem::makeEntries() { { "hylian_loach", "Legendary Catch", - "Catch a Hylian Loach.", + "Obtain the Hylian Loach in your fishing journal.", AchievementCategory::Collection, false, 0, 0, false }, @@ -280,7 +304,7 @@ std::vector AchievementSystem::makeEntries() { { "all_fish", "Gone Fishin'", - "Catch all 6 species of fish.", + "Obtain all 6 species of fish in your fishing journal.", AchievementCategory::Collection, true, 6, 0, false }, @@ -392,7 +416,7 @@ std::vector AchievementSystem::makeEntries() { false, 0, 0, false }, [](Achievement& a, json&) { - if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) { + if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() < 20) { a.progress = 1; } }, @@ -506,6 +530,29 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "rollstab_triple", + "Surgical Skewer", + "Kill 3 enemies with a single rollstab.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + static int rollstabKills = 0; + const auto* link = static_cast(daPy_getPlayerActorClass()); + const bool inRollstab = link != nullptr && link->mProcID == daAlink_c::PROC_CUT_FINISH && link->mIsRollstab; + if (!inRollstab) { + rollstabKills = 0; + return; + } + rollstabKills += AchievementSystem::get().signalCount("rollstab_kill"); + if (rollstabKills >= 3) { + a.progress = 1; + } + }, + {} + }, // Minigame { { @@ -600,6 +647,9 @@ std::vector AchievementSystem::makeEntries() { false, 0, 0, false }, [](Achievement& a, json&) { + if (strcmp(dComIfGp_getStartStageName(), "F_SP114") != 0) { + return; + } const int32_t bestMs = dComIfGs_getRaceGameTime(); if (dComIfGs_isEventBit(dSv_event_flag_c::F_0481) && bestMs > 0 && bestMs <= 70000) { @@ -681,7 +731,7 @@ std::vector AchievementSystem::makeEntries() { { "long_jump_attack", "Long Jump Attack", - "Travel more than 20 meters in a single jump attack before landing.", + "Travel more than 15 meters in a single jump attack before landing.", AchievementCategory::Misc, false, 0, 0, false }, @@ -711,7 +761,7 @@ std::vector AchievementSystem::makeEntries() { inJump = false; const float dx = link->current.pos.x - startX; const float dz = link->current.pos.z - startZ; - if (dx * dx + dz * dz >= 2000.0f * 2000.0f) { + if (dx * dx + dz * dz >= 1500.0f * 1500.0f) { a.progress = 1; } } else if (link->mProcID != daAlink_c::PROC_CUT_JUMP) { @@ -800,6 +850,66 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "ganondorf_3hit", + "Autospin Annihilation", + "Finish off Ganondorf in the final duel after only 3 attacks.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + auto& sys = AchievementSystem::get(); + const auto* link = static_cast(daPy_getPlayerActorClass()); + + static int autospinCount = 0; + static int pendingHits = 0; + static bool invalidated = false; + static bool wasInFight = false; + + auto* gnd = static_cast(fopAcM_SearchByName(fpcNm_B_GND_e)); + const bool inFight = gnd != nullptr && !gnd->checkRide(); + + if (inFight && !wasInFight) { + autospinCount = 0; + pendingHits = 0; + invalidated = false; + } + wasInFight = inFight; + + if (!inFight) { + return; + } + + const bool hitOccurred = sys.hasSignal("ganondorf_hit"); + const bool knockedDown = sys.hasSignal("ganondorf_knocked_down"); + + if (hitOccurred && knockedDown) { + // Spin completing an autospin: pendingHits should be exactly 1 (the spin attack) + if (pendingHits == 1) { + autospinCount++; + pendingHits = 0; + } else { + invalidated = true; + } + } else if (hitOccurred) { + pendingHits++; + if (pendingHits > 1) { + invalidated = true; + } + } + + if (link != nullptr && link->mProcID == daAlink_c::PROC_GANON_FINISH) { + if (!invalidated && autospinCount == 3) { + a.progress = 1; + } + autospinCount = 0; + pendingHits = 0; + invalidated = false; + } + }, + {} + }, // Glitched { { @@ -1012,6 +1122,55 @@ std::vector AchievementSystem::makeEntries() { a.progress = 1; }, {} + }, + { + { + "early_city", + "Early City", + "Obtain the Double Clawshots without obtaining the Dominion Rod.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daPy_getPlayerActorClass() == nullptr) { + return; + } + bool hasDoubleClawshot = false; + bool hasDominionRod = false; + for (int i = 0; i < 24; ++i) { + const auto item = dComIfGs_getItem(i, false); + if (item == dItemNo_W_HOOKSHOT_e) { + hasDoubleClawshot = true; + } + if (item == dItemNo_COPY_ROD_e || item == dItemNo_COPY_ROD_2_e) { + hasDominionRod = true; + } + } + if (hasDoubleClawshot && !hasDominionRod) { + a.progress = 1; + } + }, + {} + }, + { + { + "early_kakariko", + "Gorge Skip", + "Collect the Kakariko warp portal without warping the gorge bridge.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::M_018)) { + return; + } + const bool savedPortal = g_dComIfG_gameInfo.info.getSavedata().getSave(dStage_SaveTbl_ELDIN).getBit().isSwitch(31); + const bool livePortal = dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo()) == dStage_SaveTbl_ELDIN && dComIfGs_isSaveSwitch(31); + if (savedPortal || livePortal) { + a.progress = 1; + } + }, + {} } }; } @@ -1088,11 +1247,17 @@ void AchievementSystem::clearAll() { } void AchievementSystem::signal(const char* key) { - m_signals.insert(key); + m_signals[key]++; } bool AchievementSystem::hasSignal(const char* key) const { - return m_signals.count(key) > 0; + const auto it = m_signals.find(key); + return it != m_signals.end() && it->second > 0; +} + +int AchievementSystem::signalCount(const char* key) const { + const auto it = m_signals.find(key); + return it != m_signals.end() ? it->second : 0; } void AchievementSystem::clearOne(const char* key) {