Achievements improvements v2 (#1553)

* make LJA achievement more attainable glitchlessly

* update loach achievement description

* 3 kill rollstab achievement

* update gone fishin description

* gorge skip achievement

* early city achievement

* make goats and snowboarding safety check stage

* fix indomitable requirement, add hero mode achievement

* properly check skybook completion when returned

* prototype ganondorf achievement

* Autospin Annihilation
This commit is contained in:
qwertyquerty
2026-05-24 11:18:07 -07:00
committed by GitHub
parent a6376368ee
commit 8668474a33
6 changed files with 190 additions and 10 deletions
+1
View File
@@ -4564,6 +4564,7 @@ public:
cXyz mIBChainInterpCurrHandRoot;
bool mIBChainInterpPrevValid;
bool mIBChainInterpCurrValid;
bool mIsRollstab = false;
#endif
}; // Size: 0x385C
+3 -2
View File
@@ -5,7 +5,7 @@
#include <queue>
#include <string>
#include <string_view>
#include <unordered_set>
#include <unordered_map>
#include <vector>
#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<Achievement> getAchievements() const;
@@ -62,7 +63,7 @@ private:
void processEntry(Entry& e);
std::vector<Entry> m_entries;
std::unordered_set<std::string_view> m_signals;
std::unordered_map<std::string_view, int> m_signals;
bool m_loaded = false;
bool m_dirty = false;
};
+4
View File
@@ -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;
+4
View File
@@ -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;
}
+5
View File
@@ -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<const daAlink_c*>(daPy_getPlayerActorClass());
if (link != nullptr && link->mProcID == daAlink_c::PROC_CUT_FINISH && link->mIsRollstab) {
dusk::AchievementSystem::get().signal("rollstab_kill");
}
}
#endif
}
+173 -8
View File
@@ -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 <filesystem>
#include <algorithm>
@@ -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::Entry> 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<const daAlink_c*>(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::Entry> 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::Entry> 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::Entry> 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::Entry> 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::Entry> 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<const daAlink_c*>(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::Entry> 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::Entry> 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::Entry> 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::Entry> 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<const daAlink_c*>(daPy_getPlayerActorClass());
static int autospinCount = 0;
static int pendingHits = 0;
static bool invalidated = false;
static bool wasInFight = false;
auto* gnd = static_cast<b_gnd_class*>(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::Entry> 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) {