#include "ActorBehavior.h" #include #include "2s2h/ShipUtils.h" #include "2s2h/CustomMessage/CustomMessage.h" #include extern "C" { #include "functions.h" #include "variables.h" #include "overlays/actors/ovl_En_Gs/z_en_gs.h" } #define FIRST_GS_MESSAGE 0x20D1 #define SECOND_GS_MESSAGE 0x20C0 std::vector flavorText = { "Good luck on your journey ...", "I hope you find what you're looking for ...", "... Evil is afoot", "Beware the moon's gaze", " .. It's dangerous to go alone", }; // clang-format off std::unordered_map riToWeight = { { RI_SOUL_BOSS_MAJORA, 13 }, { RI_MASK_DEKU, 12 }, { RI_MASK_GORON, 12 }, { RI_MASK_ZORA, 12 }, { RI_MASK_BLAST, 11 }, { RI_MASK_FIERCE_DEITY, 11 }, { RI_SOUL_BOSS_GOHT, 10 }, { RI_SOUL_BOSS_GYORG, 10 }, { RI_SOUL_BOSS_ODOLWA, 10 }, { RI_SOUL_BOSS_TWINMOLD, 10 }, { RI_REMAINS_GOHT, 10 }, { RI_REMAINS_GYORG, 10 }, { RI_REMAINS_ODOLWA, 10 }, { RI_REMAINS_TWINMOLD, 10 }, }; std::unordered_map rcToWeight = { { RC_PINNACLE_ROCK_REUNITE_SEAHORSE, 10 }, { RC_GREAT_BAY_COAST_NEW_WAVE_BOSSA_NOVA, 10 }, { RC_MOUNTAIN_VILLAGE_FROG_CHOIR, 10 }, { RC_STOCK_POT_INN_COUPLES_MASK, 10 }, { RC_ROMANI_RANCH_ALIENS, 10 }, { RC_WATERFALL_RAPIDS_BEAVER_RACE_01, 8 }, { RC_WATERFALL_RAPIDS_BEAVER_RACE_02, 8 }, { RC_KEATON_QUIZ, 8 }, { RC_CURIOSITY_SHOP_SPECIAL_ITEM, 8 }, { RC_DEKU_PLAYGROUND_ALL_DAYS, 8 }, { RC_MOON_TRIAL_ZORA_PIECE_OF_HEART, 6 }, { RC_MOON_TRIAL_DEKU_PIECE_OF_HEART, 6 }, { RC_MOON_TRIAL_GORON_PIECE_OF_HEART, 6 }, }; std::unordered_map itemTypeToWeight = { { RITYPE_MAJOR, 9 }, { RITYPE_MASK, 9 }, { RITYPE_BOSS_KEY, 8 }, { RITYPE_LESSER, 6 }, { RITYPE_SMALL_KEY, 5 }, { RITYPE_SKULLTULA_TOKEN, 3 }, { RITYPE_STRAY_FAIRY, 3 }, { RITYPE_HEALTH, 2 }, { RITYPE_JUNK, 2 }, }; // clang-format on s32 GetNormalizedCost() { s32 obtainedChecks = 0; s32 maxChecks = 0; for (auto& [randoCheckId, _] : Rando::StaticData::Checks) { RandoSaveCheck saveCheck = RANDO_SAVE_CHECKS[randoCheckId]; if (saveCheck.shuffled) { maxChecks++; if (saveCheck.obtained) { obtainedChecks++; } } } return MAX(10, MIN(250, 10 + (obtainedChecks * (250 - 10)) / (maxChecks))); } RandoCheckId GetRandomCheck(bool repeatableOnlyObtained = false) { Player* player = GET_PLAYER(gPlayState); if (player->talkActor == nullptr || player->talkActor->id != ACTOR_EN_GS) { return RC_UNKNOWN; } EnGs* enGs = (EnGs*)player->talkActor; u32 strength = RANDO_SAVE_OPTIONS[RO_HINTS_GOSSIP_STONE_STRENGTH]; std::vector> weightedChecks; u32 totalWeight = 0; for (auto& [randoCheckId, _] : Rando::StaticData::Checks) { RandoSaveCheck saveCheck = RANDO_SAVE_CHECKS[randoCheckId]; auto& item = Rando::StaticData::Items[saveCheck.randoItemId]; if (!saveCheck.shuffled || item.randoItemType == RITYPE_JUNK || (repeatableOnlyObtained && saveCheck.obtained)) { continue; } u32 baseWeight = 1; if (rcToWeight.contains(randoCheckId)) { baseWeight = rcToWeight[randoCheckId]; } else if (riToWeight.contains(saveCheck.randoItemId)) { baseWeight = riToWeight[saveCheck.randoItemId]; } else if (itemTypeToWeight.contains(item.randoItemType)) { baseWeight = itemTypeToWeight[item.randoItemType]; } u32 effectiveWeight = 100 + (baseWeight - 1) * strength; totalWeight += effectiveWeight; weightedChecks.push_back({ randoCheckId, totalWeight }); } if (weightedChecks.empty()) { return RC_UNKNOWN; } if (repeatableOnlyObtained) { Ship_Random_Seed(gGameState->frames); } else { uint32_t seed = gPlayState->sceneId + enGs->actor.home.pos.x + enGs->actor.home.pos.z; Ship_Random_Seed(gSaveContext.save.shipSaveInfo.rando.finalSeed + seed); } u32 roll = Ship_Random(0, totalWeight); for (auto& [checkId, cumWeight] : weightedChecks) { if (roll < cumWeight) { return checkId; } } return weightedChecks.back().first; } void Rando::ActorBehavior::InitEnGsBehavior() { bool shouldRegister = IS_RANDO && (RANDO_SAVE_OPTIONS[RO_HINTS_GOSSIP_STONES] || RANDO_SAVE_OPTIONS[RO_HINTS_PURCHASEABLE]); COND_VB_SHOULD(VB_GS_CONSIDER_MASK_OF_TRUTH_EQUIPPED, shouldRegister, { *should = true; }); // Override the message ID so that we can control the text COND_VB_SHOULD(VB_GS_CONTINUE_TEXTBOX, shouldRegister, { *should = false; Message_ContinueTextbox(gPlayState, SECOND_GS_MESSAGE); }); COND_ID_HOOK(OnOpenText, FIRST_GS_MESSAGE, shouldRegister, [](u16* textId, bool* loadFromMessageTable) { auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId); if (RANDO_SAVE_OPTIONS[RO_HINTS_GOSSIP_STONES]) { RandoCheckId randoCheckId = GetRandomCheck(); if (randoCheckId == RC_UNKNOWN) { return; } entry.autoFormat = false; auto& saveCheck = RANDO_SAVE_CHECKS[randoCheckId]; bool showExact = false; if (rcToWeight.contains(randoCheckId)) { showExact = true; } entry.msg = "They say %g{{item}}%w is hidden %y{{location}}%w."; CustomMessage::Replace(&entry.msg, "{{item}}", Rando::StaticData::GetItemName(saveCheck.randoItemId, true, randoCheckId)); CustomMessage::Replace(&entry.msg, "{{location}}", Rando::StaticData::GetLocationNameForHint(randoCheckId, showExact)); // Replace colors before line break calculation CustomMessage::ReplaceColorChars(&entry.msg); CustomMessage::AddLineBreaks(&entry.msg); if (RANDO_SAVE_OPTIONS[RO_HINTS_PURCHASEABLE]) { entry.msg += "\x10...\x13\x12"; } } else { entry.msg = ""; } if (RANDO_SAVE_OPTIONS[RO_HINTS_PURCHASEABLE]) { entry.msg += "Trade %r{{rupees}} Rupees%w for a hint?\x02\x11\xC2No\x11Yes"; s32 cost = GetNormalizedCost(); CustomMessage::Replace(&entry.msg, "{{rupees}}", std::to_string(cost)); CustomMessage::ReplaceColorChars(&entry.msg); } CustomMessage::EnsureMessageEnd(&entry.msg); CustomMessage::LoadCustomMessageIntoFont(entry); *loadFromMessageTable = false; }); COND_ID_HOOK(OnOpenText, SECOND_GS_MESSAGE, shouldRegister, [](u16* textId, bool* loadFromMessageTable) { MessageContext* msgCtx = &gPlayState->msgCtx; auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId); if (RANDO_SAVE_OPTIONS[RO_HINTS_PURCHASEABLE]) { if (msgCtx->choiceIndex == 1) { s32 cost = GetNormalizedCost(); RandoCheckId randoCheckId = GetRandomCheck(true); if (gSaveContext.save.saveInfo.playerData.rupees < cost) { entry.msg = "Foolish... You don't have enough rupees..."; } else if (randoCheckId == RC_UNKNOWN) { entry.msg = "I have no more hints for you..."; } else { RandoSaveCheck saveCheck = RANDO_SAVE_CHECKS[randoCheckId]; entry.msg = "Wise choice... They say %g{{item}}%w is hidden %y{{location}}%w."; CustomMessage::Replace(&entry.msg, "{{item}}", Rando::StaticData::GetItemName(saveCheck.randoItemId, true, randoCheckId)); CustomMessage::Replace(&entry.msg, "{{location}}", Rando::StaticData::GetLocationNameForHint(randoCheckId, true)); gSaveContext.rupeeAccumulator -= cost; cost *= 2; } } else { entry.msg = "Foolish... Come back later when you have more sense."; } } else { entry.msg = flavorText[Ship_Random(0, flavorText.size())]; } CustomMessage::LoadCustomMessageIntoFont(entry); *loadFromMessageTable = false; }); // Four Gossip Stone Grottos Heart Piece item grant behavior override COND_VB_SHOULD(VB_GIVE_ITEM_FROM_OFFER, IS_RANDO, { GetItemId* item = va_arg(args, GetItemId*); Actor* refActor = va_arg(args, Actor*); Player* player = GET_PLAYER(gPlayState); if (refActor->id != ACTOR_EN_GS || *item != GI_HEART_PIECE) { return; } *should = false; refActor->parent = &player->actor; }); }