diff --git a/include/d/d_com_inf_game.h b/include/d/d_com_inf_game.h index 964cc34c0f..d81fc5bc1e 100644 --- a/include/d/d_com_inf_game.h +++ b/include/d/d_com_inf_game.h @@ -1845,6 +1845,12 @@ inline void dComIfGs_addDeathCount() { g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().addDeathCount(); } +#if TARGET_PC +inline u16 dComIfGs_getDeathCount() { + return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getDeathCount(); +} +#endif + inline char* dComIfGs_getPlayerName() { return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getPlayerName(); } diff --git a/include/d/d_save.h b/include/d/d_save.h index 0e9a4b37cd..e2719e0a98 100644 --- a/include/d/d_save.h +++ b/include/d/d_save.h @@ -486,6 +486,9 @@ public: mDeathCount++; } } +#if TARGET_PC + u16 getDeathCount() const { return mDeathCount; } +#endif char* getPlayerName() const { return const_cast(mPlayerName); } void setPlayerName(const char* i_name) { #if AVOID_UB diff --git a/src/d/actor/d_a_alink_damage.inc b/src/d/actor/d_a_alink_damage.inc index e095fc0393..bccdbf7381 100644 --- a/src/d/actor/d_a_alink_damage.inc +++ b/src/d/actor/d_a_alink_damage.inc @@ -205,6 +205,9 @@ int daAlink_c::setDamagePoint(int i_dmgAmount, BOOL i_checkZoraMag, BOOL i_setDm dComIfGp_setItemLifeCount(-i_dmgAmount, 0); } +#if TARGET_PC + dusk::AchievementSystem::get().signal("player_damaged"); +#endif onResetFlg1(RFLG1_DAMAGE_IMPACT); mSwordUpTimer = 0; diff --git a/src/d/actor/d_a_arrow.cpp b/src/d/actor/d_a_arrow.cpp index 769c3273e9..c18d19e149 100644 --- a/src/d/actor/d_a_arrow.cpp +++ b/src/d/actor/d_a_arrow.cpp @@ -17,6 +17,9 @@ #include "d/actor/d_a_e_pz.h" #include "d/actor/d_a_horse.h" #include "d/actor/d_a_hozelda.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif int daArrow_c::createHeap() { J3DModelData* model_data; @@ -92,7 +95,12 @@ void daArrow_c::atHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, d if (dist_to_hitpos < field_0x998) { field_0x998 = dist_to_hitpos; mHitAcID = fopAcM_GetID(i_tgActor); - +#if TARGET_PC + if (fopAcM_GetGroup(i_tgActor) == fopAc_ENEMY_e && + current.pos.abs(mStartPos) > 10000.0f) { + dusk::AchievementSystem::get().signal("arrow_hit_100m"); + } +#endif if (mArrowType == 1) { field_0x9a8 = *hit_pos_p; } else if (i_tgObjInf->ChkTgShield()) { diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index cd6bce78c4..f5b03be1b1 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -19,6 +19,9 @@ #include "dusk/frame_interpolation.h" #include "dusk/settings.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif class daB_GND_HIO_c : public JORReflexible { public: @@ -1289,6 +1292,9 @@ static void b_gnd_g_wait(b_gnd_class* i_this) { if (i_this->mMoveMode < 5 && i_this->mPlayerDistXZ < 600.0f) { i_this->mMoveMode = 5; i_this->field_0xc44[0] = 10; +#if TARGET_PC + dusk::AchievementSystem::get().signal("ganondorf_fishing_rod"); +#endif } } else if (i_this->mMoveMode == 5) { i_this->mMoveMode = 6; diff --git a/src/d/actor/d_a_e_th.cpp b/src/d/actor/d_a_e_th.cpp index 43ad8ec19b..056e9f565e 100644 --- a/src/d/actor/d_a_e_th.cpp +++ b/src/d/actor/d_a_e_th.cpp @@ -12,6 +12,9 @@ #include "c/c_damagereaction.h" #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_camera_mng.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif class daE_TH_HIO_c : public JORReflexible { public: @@ -542,6 +545,7 @@ static void damage_check(e_th_class* i_this) { if (i_this->field_0x6a4 == 0 && i_this->mAction != ACTION_SPIN) { daPy_py_c* player = (daPy_py_c*)dComIfGp_getPlayer(0); OS_REPORT("E_th HP1 %d\n", i_this->health); + s16 prevHealth = i_this->health; cc_at_check(i_this, &i_this->mAtInfo); OS_REPORT("E_th HP2 %d\n", i_this->health); @@ -554,6 +558,11 @@ static void damage_check(e_th_class* i_this) { dComIfGs_onOneZoneSwitch(3, -1); if (i_this->health <= 0) { +#if TARGET_PC + if (prevHealth == i_this->field_0x560) { + dusk::AchievementSystem::get().signal("dark_hammer_one_hit"); + } +#endif i_this->mAction = ACTION_END; i_this->mMode = 0; i_this->field_0x68a |= 4; diff --git a/src/d/d_cc_uty.cpp b/src/d/d_cc_uty.cpp index 7d71e490ac..ec9a177683 100644 --- a/src/d/d_cc_uty.cpp +++ b/src/d/d_cc_uty.cpp @@ -13,6 +13,9 @@ #include "d/d_s_play.h" #include "d/d_com_inf_game.h" #include "f_op/f_op_actor_mng.h" +#if TARGET_PC +#include "dusk/achievements.h" +#endif static int plCutLRC[58] = { 0, // @@ -434,6 +437,11 @@ fopAc_ac_c* cc_at_check(fopAc_ac_c* i_enemy, dCcU_AtInfo* i_AtInfo) { if (i_AtInfo->mAttackPower != 0 && i_enemy->health <= 0) { i_AtInfo->mHitStatus = 2; i_enemy->health = 0; +#if TARGET_PC + if (fopAcM_GetGroup(i_enemy) == fopAc_ENEMY_e) { + dusk::AchievementSystem::get().signal("enemy_killed"); + } +#endif } int uvar8; diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 26a302678e..637dd13980 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -2,11 +2,16 @@ #include "dusk/io.hpp" #include "dusk/main.h" #include "d/d_com_inf_game.h" +#include "d/d_item_data.h" +#include "d/d_map_path_fmap.h" +#include "d/d_stage.h" +#include "d/d_menu_fmap.h" +#include "JSystem/JKernel/JKRArchive.h" #include "d/d_meter2_info.h" #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_player.h" -#include "d/d_demo.h" #include "f_pc/f_pc_name.h" #include "f_op/f_op_actor_mng.h" @@ -17,6 +22,14 @@ namespace dusk { using json = nlohmann::json; +static void* s_cucco_play_search(void* i_actor, void*) { + if (!fopAcM_IsActor(i_actor) || fopAcM_GetName((fopAc_ac_c*)i_actor) != fpcNm_NI_e) { + return nullptr; + } + auto* ni = static_cast(i_actor); + return ni->mAction == ACTION_PLAY_e ? i_actor : nullptr; +} + static void checkGoatHerding(Achievement& a, int32_t threshMs) { if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) { return; @@ -31,6 +44,7 @@ static constexpr auto ACHIEVEMENTS_FILENAME = "achievements.json"; std::vector AchievementSystem::makeEntries() { return { + // Story { { "hero_of_twilight", @@ -47,6 +61,433 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "completionist", + "Completionist", + "100% the game.", + AchievementCategory::Story, + 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() < 100) { + return; + } + for (int i = 0; i < 4; ++i) { + if (!dComIfGs_isCollectMirror(i)) { + return; + } + } + for (int i = 0; i < 3; ++i) { + if (!dComIfGs_isCollectCrystal(i)) { + return; + } + } + static const u16 skillBits[] = { + dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339, + dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341, + dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343, + dSv_event_flag_c::F_0344 + }; + for (u16 bit : skillBits) { + if (!dComIfGs_isEventBit(bit)) { + return; + } + } + if (dComIfGs_checkGetInsectNum() < 24) { + return; + } + if (dComIfGs_getPohSpiritNum() < 60) { + return; + } + if (dComIfGs_getWalletSize() < 2) { + return; + } + if (dComIfGs_getArrowMax() < 100) { + return; + } + if (!dComIfGs_isCollectSword(COLLECT_MASTER_SWORD)) { + return; + } + if (!dComIfGs_isCollectShield(COLLECT_HYLIAN_SHIELD)) { + return; + } + if (!dComIfGs_isCollectClothes(KOKIRI_CLOTHES_FLAG)) { + return; + } + if (!dComIfGs_isItemFirstBit(dItemNo_WEAR_ZORA_e)) { + return; + } + if (!dComIfGs_isItemFirstBit(dItemNo_ARMOR_e)) { + return; + } + static const struct { int stage; int sw; } warpPortals[] = { + { dStage_SaveTbl_ORDON, 52 }, // Ordon Spring + { dStage_SaveTbl_FARON, 71 }, // South Faron Woods + { dStage_SaveTbl_FARON, 2 }, // North Faron Woods + { dStage_SaveTbl_GROVE, 100 }, // Sacred Grove + { dStage_SaveTbl_FIELD, 21 }, // Gorge + { dStage_SaveTbl_ELDIN, 31 }, // Kakariko Village + { dStage_SaveTbl_ELDIN, 21 }, // Death Mountain + { dStage_SaveTbl_FIELD, 99 }, // Bridge of Eldin + { dStage_SaveTbl_FIELD, 3 }, // Castle Town + { dStage_SaveTbl_LANAYRU, 10 }, // Lake Hylia + { dStage_SaveTbl_LANAYRU, 2 }, // Zora's Domain + { dStage_SaveTbl_LANAYRU, 21 }, // Upper Zora's River + { dStage_SaveTbl_SNOWPEAK, 21 }, // Snowpeak + { dStage_SaveTbl_DESERT, 21 }, // Gerudo Mesa + { dStage_SaveTbl_DESERT, 40 }, // Mirror Chamber + }; + for (const auto& p : warpPortals) { + if (!g_dComIfG_gameInfo.info.getSavedata().getSave(p.stage).getBit().isSwitch(p.sw)) { + return; + } + } + if (dComIfGs_getCollectSmell() == dItemNo_NONE_e) { + return; + } + + if (dMeter2Info_getRecieveLetterNum() == 0) { + return; + } + + bool hasJournal = false; + for (int fi = 0; fi < 6; ++fi) { + if (dComIfGs_getFishNum(fi) != 0) { + hasJournal = true; + break; + } + } + if (!hasJournal) { + return; + } + + int bottleCount = 0; + for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) { + if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) { + bottleCount++; + } + } + if (bottleCount < 4) { + return; + } + + int bombBagCount = 0; + for (int i = 0; i < dSv_player_item_c::BOMB_BAG_MAX; ++i) { + if (dComIfGs_getItem(SLOT_15 + i, false) != dItemNo_NONE_e) { + bombBagCount++; + } + } + if (bombBagCount < 3) { + return; + } + + bool hasJewelRod = false; + for (int slot = 0; slot < 24 && !hasJewelRod; ++slot) { + const u8 item = dComIfGs_getItem(slot, false); + if (item == dItemNo_JEWEL_ROD_e || item == dItemNo_JEWEL_BEE_ROD_e || item == dItemNo_JEWEL_WORM_ROD_e) { + hasJewelRod = true; + } + } + if (!hasJewelRod) { + return; + } + + static const u8 requiredWheelItems[] = { + dItemNo_BOOMERANG_e, + dItemNo_BOW_e, + dItemNo_W_HOOKSHOT_e, + dItemNo_SPINNER_e, + dItemNo_IRONBALL_e, + dItemNo_COPY_ROD_e, + dItemNo_HVY_BOOTS_e, + dItemNo_KANTERA_e, + dItemNo_PACHINKO_e, + dItemNo_HAWK_EYE_e, + dItemNo_ANCIENT_DOCUMENT_e, + dItemNo_HORSE_FLUTE_e, + }; + for (u8 required : requiredWheelItems) { + bool found = false; + for (int slot = 0; slot < 24; ++slot) { + if (dComIfGs_getItem(slot, false) == required) { + found = true; + break; + } + } + if (!found) { + return; + } + } + a.progress = 1; + }, + {} + }, + // Collection + { + { + "princess_of_bugs", + "The Princess of Bugs", + "Deliver all 24 golden bugs to Agitha.", + AchievementCategory::Collection, + true, 24, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_checkGetInsectNum(); + }, + {} + }, + { + { + "all_poes", + "Poe Collector", + "Collect all 60 Poe Souls.", + AchievementCategory::Collection, + true, 60, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_getPohSpiritNum(); + }, + {} + }, + { + { + "hylian_loach", + "Legendary Catch", + "Catch a Hylian Loach.", + AchievementCategory::Collection, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_getFishNum(1) > 0) { + a.progress = 1; + } + }, + {} + }, + { + { + "all_fish", + "Gone Fishin'", + "Catch all 6 species of fish.", + AchievementCategory::Collection, + true, 6, 0, false + }, + [](Achievement& a, json&) { + int nUniqueFish = 0; + for (int i = 0; i < 6; ++i) { + if (dComIfGs_getFishNum(i) != 0) { + nUniqueFish++; + } + } + a.progress = nUniqueFish; + }, + {} + }, + { + { + "a_big_heart", + "A Big Heart", + "Reach maximum health with all 20 heart containers.", + AchievementCategory::Collection, + true, 20, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_getMaxLife() / 5; + }, + {} + }, + { + { + "all_bottles", + "Glassware Guardian", + "Obtain all 4 bottles.", + AchievementCategory::Collection, + true, 4, 0, false + }, + [](Achievement& a, json&) { + if (daPy_getPlayerActorClass() == nullptr) { + return; + } + int count = 0; + for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) { + if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) { + count++; + } + } + a.progress = count; + }, + {} + }, + { + { + "all_hidden_skills", + "Master of Secrets", + "Learn all 7 Hidden Skills.", + AchievementCategory::Collection, + true, 7, 0, false + }, + [](Achievement& a, json&) { + static const u16 skillBits[] = { + dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339, + dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341, + dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343, + dSv_event_flag_c::F_0344 + }; + int count = 0; + for (u16 bit : skillBits) { + if (dComIfGs_isEventBit(bit)) { + count++; + } + } + a.progress = count; + }, + {} + }, + // Challenge + { + { + "cave_of_ordeals", + "Conqueror of Ordeals", + "Clear all 50 floors of the Cave of Ordeals.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daNpcF_chkEvtBit(0x1F9)) { + a.progress = 1; + } + }, + {} + }, + { + { + "cave_of_ordeals_heartless", + "Indomitable", + "Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) { + a.progress = 1; + } + }, + {} + }, + { + { + "speedrun_12h", + "Been There Done That", + "Defeat Ganondorf with a total save file play time under 12 hours.", + 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; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 12 * 3600) { + a.progress = 1; + } + }, + {} + }, + { + { + "speedrun_8h", + "Swift Blade", + "Defeat Ganondorf with a total save file play time under 6 hours.", + 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; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 8 * 3600) { + a.progress = 1; + } + }, + {} + }, + { + { + "dark_hammer_one_hit", + "Mortal Edge", + "Defeat Dark Hammer in a single hit.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("dark_hammer_one_hit")) { + a.progress = 1; + } + }, + {} + }, + { + { + "no_deaths_clear", + "Deathless", + "Defeat Ganondorf with 0 deaths on your save file.", + 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_getDeathCount() == 0) { + a.progress = 1; + } + }, + {} + }, + { + { + "untouchable", + "Untouchable", + "Kill 25 enemies in a row without taking damage.", + AchievementCategory::Challenge, + true, 25, 0, false + }, + [](Achievement& a, json&) { + auto& sys = AchievementSystem::get(); + if (sys.hasSignal("player_damaged")) { + a.progress = 0; + } + if (sys.hasSignal("enemy_killed")) { + a.progress++; + } + }, + {} + }, + { + { + "bow_100m_hit", + "Long Shot", + "Hit an enemy from over 100 meters away with the bow.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("arrow_hit_100m")) { + a.progress = 1; + } + }, + {} + }, + // Minigame { { "plumm_max", @@ -133,14 +574,16 @@ std::vector AchievementSystem::makeEntries() { }, { { - "cave_of_ordeals", - "Conqueror of Ordeals", - "Clear all 50 floors of the Cave of Ordeals.", - AchievementCategory::Challenge, + "snowboard_70s", + "Downhill Dash", + "Finish the snowboarding minigame in under 70 seconds.", + AchievementCategory::Minigame, false, 0, 0, false }, [](Achievement& a, json&) { - if (daNpcF_chkEvtBit(0x1F9)) { + const int32_t bestMs = dComIfGs_getRaceGameTime(); + if (dComIfGs_isEventBit(dSv_event_flag_c::F_0481) && + bestMs > 0 && bestMs <= 70000) { a.progress = 1; } }, @@ -148,132 +591,28 @@ std::vector AchievementSystem::makeEntries() { }, { { - "cave_of_ordeals_heartless", - "Indomitable", - "Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.", - AchievementCategory::Challenge, - false, 0, 0, false - }, - [](Achievement& a, json&) { - if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) { - a.progress = 1; - } - }, - {} - }, - { - { - "speedrun_12h", - "Been There Done That", - "Defeat Ganondorf with a total save file play time under 12 hours.", - AchievementCategory::Challenge, + "canoe_perfect", + "River Raider", + "Achieve a perfect score in the canoe minigame.", + AchievementCategory::Minigame, false, 0, 0, false }, [](Achievement& a, json&) { const auto* link = static_cast(daPy_getPlayerActorClass()); - if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + if (link == nullptr) { return; } - const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); - if (ticks / OS_TIMER_CLOCK < 12 * 3600) { + static bool wasInCanoe = false; + bool inCanoe = link->mProcID >= daAlink_c::PROC_CANOE_RIDE && + link->mProcID <= daAlink_c::PROC_CANOE_KANDELAAR_POUR; + if (wasInCanoe && !inCanoe && dMeter2Info_getNowCount() >= 30) { a.progress = 1; } + wasInCanoe = inCanoe; }, {} }, - { - { - "speedrun_8h", - "Swift Blade", - "Defeat Ganondorf with a total save file play time under 6 hours.", - 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; - } - const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); - if (ticks / OS_TIMER_CLOCK < 8 * 3600) { - a.progress = 1; - } - }, - {} - }, - { - { - "princess_of_bugs", - "The Princess of Bugs", - "Deliver all 24 golden bugs to Agitha.", - AchievementCategory::Collection, - true, 24, 0, false - }, - [](Achievement& a, json&) { - a.progress = dComIfGs_checkGetInsectNum(); - }, - {} - }, - { - { - "all_poes", - "Poe Collector", - "Collect all 60 Poe Souls.", - AchievementCategory::Collection, - true, 60, 0, false - }, - [](Achievement& a, json&) { - a.progress = dComIfGs_getPohSpiritNum(); - }, - {} - }, - { - { - "hylian_loach", - "Legendary Catch", - "Catch a Hylian Loach.", - AchievementCategory::Collection, - false, 0, 0, false - }, - [](Achievement& a, json&) { - if (dComIfGs_getFishNum(1) > 0) { - a.progress = 1; - } - }, - {} - }, - { - { - "all_fish", - "Gone Fishin'", - "Catch all 6 species of fish.", - AchievementCategory::Collection, - true, 6, 0, false - }, - [](Achievement& a, json&) { - int nUniqueFish = 0; - for (int i = 0; i < 6; ++i) { - if (dComIfGs_getFishNum(i) != 0) { - nUniqueFish++; - } - } - a.progress = nUniqueFish; - }, - {} - }, - { - { - "a_big_heart", - "A Big Heart", - "Reach maximum health with all 20 heart containers.", - AchievementCategory::Collection, - true, 20, 0, false - }, - [](Achievement& a, json&) { - a.progress = dComIfGs_getMaxLife() / 5; - }, - {} - }, + // Misc { { "friendly_fire", @@ -326,6 +665,87 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "email_me", + "Email Me", + "Read a letter during the Dark Beast Ganon fight.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e); + if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) { + a.progress = 1; + } + }, + {} + }, + { + { + "heavy_hitter", + "Heavy Hitter", + "Wear the Iron Boots during the end credits.", + AchievementCategory::Misc, + 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 (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) { + a.progress = 1; + } + }, + {} + }, + { + { + "fishing_rod_ganondorf", + "Here Fishy Fishy", + "Confuse Ganondorf with the fishing rod.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("ganondorf_fishing_rod")) { + a.progress = 1; + } + }, + {} + }, + { + { + "steal_from_trill", + "Petty Theft", + "Steal from Trill.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::F_0758)) { + a.progress = 1; + } + }, + {} + }, + { + { + "cucco_control", + "Cucco Whisperer", + "Take control of a cucco.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (fopAcM_Search(s_cucco_play_search, nullptr) != nullptr) { + a.progress = 1; + } + }, + {} + }, + // Glitched { { "back_in_time", @@ -411,34 +831,79 @@ std::vector AchievementSystem::makeEntries() { }, { { - "email_me", - "Email Me", - "Read a letter during the Dark Beast Ganon fight.", - AchievementCategory::Misc, + "no_fish_suit", + "No Fish Suit No Problem", + "Defeat Morpheel without equipping Zora Armor.", + AchievementCategory::Glitched, false, 0, 0, false }, [](Achievement& a, json&) { - void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e); - if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) { + static bool prevMorpheelAlive = false; + static bool inArena = false; + static bool zoraWorn = false; + const bool morpheelAlive = fopAcM_SearchByName(fpcNm_B_OH_e) != nullptr; + const bool lakebedCleared = dComIfGs_isEventBit(dSv_event_flag_c::M_045); + + if (morpheelAlive && !lakebedCleared) { + inArena = true; + if (daPy_py_c::checkZoraWearFlg()) { + zoraWorn = true; + } + } + + if (prevMorpheelAlive && !morpheelAlive && inArena && !zoraWorn) { a.progress = 1; } + + prevMorpheelAlive = morpheelAlive; + }, + {} + }, + { + { + "null_item", + "Null Item", + "Obtain the mysterious black rupee in the item wheel.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daPy_getPlayerActorClass() == nullptr) { + return; + } + for (int i = 0; i < 24; ++i) { + if (dComIfGs_getItem(i, false) == 0x00) { + a.progress = 1; + break; + } + } }, {} }, { { - "heavy-hitter", - "Heavy Hitter", - "Wear the Iron Boots during the end credits.", - AchievementCategory::Misc, + "stallord_skip", + "Stallord Skip", + "Leave Stallord's arena through the exit without defeating Stallord.", + AchievementCategory::Glitched, false, 0, 0, false }, [](Achievement& a, json&) { - const auto* link = static_cast(daPy_getPlayerActorClass()); - if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + static bool seenStallord = false; + if (strcmp(dComIfGp_getStartStageName(), "D_MN10A") != 0) { + seenStallord = false; return; } - if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) { + if (dComIfGs_isEventBit(dSv_event_flag_c::F_0265)) { + seenStallord = false; + return; + } + if (fopAcM_SearchByName(fpcNm_B_DS_e) != nullptr) { + seenStallord = true; + } + if (seenStallord && + dComIfGp_isEnableNextStage() && + strcmp(dComIfGp_getNextStageName(), "F_SP125") == 0) { a.progress = 1; } },