From 5684c793126f23ea87597fceea0420c9245a1505 Mon Sep 17 00:00:00 2001 From: Tom Overton Date: Thu, 1 Feb 2024 20:10:48 -0800 Subject: [PATCH] BossHakugin (Goht) OK and documented, EnHakurock documented, other assorted documentation (#1599) * Fix struct * 20/79 OK * BossHakugin_Draw OK * func_80B0D69C OK * func_80B0D2B8 OK * func_80B0D750 OK * Shadow function docs * Fix some non-matchings * Apparently this matches now? It didn't before * OverrideLimbDraw OK * BossHakugin_PostLimbDraw * Animation Cleanup: En_G* (#1395) * wip * more cleanup * more cleanup * one more thing * one more default case * missed brackets * PR Review * add more matches and fill stuct * 3 more Draw functions * func_80B0E5A4 * Finshed the Draw functions * Small cleanup * Most of the death-handling code done * more matching * functions * Goht collab (#9) * Animation Cleanup: En_G* (#1395) * wip * more cleanup * more cleanup * one more thing * one more default case * missed brackets * PR Review * z_message_nes (1 NON_MATCHING) (#1394) * Bring over progress * Some docs * scratch urls + format * Fixes * Match Message_DrawTextNES * String macros * color structs * Document object_mk (#1389) * Updated object_mk animation, limb, and skeleton names * Updated object_mk texture and DList names * Added comment to object_mk.xml * Remove "ing" from Anim names Co-authored-by: engineer124 <47598039+engineer124@users.noreply.github.com> * Update z_en_mk.c to not use "ing" in names * Running formatter --------- Co-authored-by: engineer124 <47598039+engineer124@users.noreply.github.com> * Player Docs: Initial framework for "Action Change Lists" (#1397) * copy fig docs * adjust wording * another fix * fix bool * PR Suggestion * Animation Cleanup: En_H* (#1399) * begin H * more cleanup * Animation Cleanup: En_I* (#1400) * cleanup I * oops * Start Player Item Docs (#1396) * Copy Fig Docs * small cleanup * ItemChangeType comment * bool * item change comments * PR Review * rm comments * Animation Cleanup: En_K* (#1402) * cleanup k * one more name * Animation Cleanup: En_J* (#1401) * cleanup J * small fix * functions --------- Co-authored-by: engineer124 <47598039+engineer124@users.noreply.github.com> Co-authored-by: Derek Hensley Co-authored-by: Zach North Co-authored-by: petrie911 * hotfix * all functions decomped * all functions decomped * Now with more data * small fix * another small fix * ok last small fix I swear * cleanup * func_80B0A8C4 OK * func_80B0D9CC OK * Use generated reloc for Goht + remove Goht's undefined_syms * Bodyparts + other clean up * cleanup * oops * fix merge * begin docs * fix merge * cutscene docs * effects docs * small docs * Document colliders * Odds and ends in the struct * Name some functions * Name two functions * Some action functions and other odds and ends * Name the lightning segments and electric ball functions correctly * Names and other cleanup * Start on hakurock + more cleanup * Name all hakurock functions * Name hakurock effects + create a GET_TYPE macro for it * Finish documenting Hakurock * Name one more function in BossHakugin * Name a few more things * Name a lot of struct vars * Wall-related stuff * Fix some fake matches * Name a couple more struct vars * Document some more of the struct * Name the limb flag stuff * Name the last unnamed struct vars * Name the horn colliders correctly * Some initial work on documenting data * Some function documentation * A couple of cleanups * Finish documenting BossHakugin_UpdateBaseRot * Name BossHakugin_SetLightningSegmentColliderVertices * Document BossHakugin_AddLightningSegments * Document BossHakugin_RunUpdateCommon and related functions * Document BossHakugin_AddMalfunctionEffects * Document BossHakugin_StepVectorToTarget * Rename to BossHakugin_StepVector * Finish documenting BossHakugin_AddMalfunctionEffects for now, might come back later though * Document BossHakugin_UpdateSubCam * Document BossHakugin_Thaw * Document BossHakugin_ChargeUpAttack * Some scattered docs * Finish documenting the top "library" functions * Document intro cutscene functions * Document running and charging * Document downed * Document throwing * Document remaining non-death action functions * Document most of the death cutscene * Get started on documenting crushing rocks * Separate out the crushing rocks into their own struct * Document the ExplosionLimbHideInfo * Finish documenting BossHakugin_DeathCutsceneCrushedByRocks * Almost done with rocks I think * Port over Odolwa shadow documentation * Finish documenting draw functions * Finish documenting BossHakugin_Update * Name variables in BossHakugin_UpdateElectricBalls * Document BossHakugin_CheckForBodyColliderHit * Finish documenting BossHakugin_UpdateDamage * I've convinced myself these names are fine * Almost finish documenting every update function * Remove TODOs from the header * Finish documenting the C file * Delete duplicate define * Finish variables.txt for Goht * Undo change that desync'd z_eff_ss_fhg_flash.h from other effect headers * Missed a THIS * Some low-hanging review responses * Add comment explaining something in BossHakugin_ShouldWait * Match BossHakugin_SpawnLargeStalactiteWalls using a for-loop * Create GOHT_ELECTRIC_BALL_COUNT_MAX constant * vector -> norm and targetVector -> targetNorm * Add TODO comments for dynamic shadow functions --------- Co-authored-by: Derek Hensley Co-authored-by: engineer124 <47598039+engineer124@users.noreply.github.com> Co-authored-by: Parker B <20159000+jpburnett@users.noreply.github.com> Co-authored-by: petrie911 Co-authored-by: petrie911 <69443847+petrie911@users.noreply.github.com> Co-authored-by: Zach North Co-authored-by: engineer124 --- include/z64save.h | 20 +- include/z64subs.h | 2 +- spec | 3 +- src/code/z_parameter.c | 18 +- src/code/z_sub_s.c | 8 +- src/overlays/actors/ovl_Boss_01/z_boss_01.c | 5 +- src/overlays/actors/ovl_Boss_02/z_boss_02.c | 4 +- src/overlays/actors/ovl_Boss_03/z_boss_03.c | 5 +- src/overlays/actors/ovl_Boss_04/z_boss_04.c | 4 +- src/overlays/actors/ovl_Boss_06/z_boss_06.c | 2 +- .../actors/ovl_Boss_Hakugin/z_boss_hakugin.c | 4046 +++++++++++++++-- .../actors/ovl_Boss_Hakugin/z_boss_hakugin.h | 181 +- .../actors/ovl_En_Hakurock/z_en_hakurock.c | 323 +- .../actors/ovl_En_Hakurock/z_en_hakurock.h | 12 +- src/overlays/actors/ovl_En_Sda/z_en_sda.c | 1 + .../actors/ovl_En_Tanron2/z_en_tanron2.c | 2 +- .../z_eff_ss_fhg_flash.c | 21 +- .../z_eff_ss_fhg_flash.h | 11 +- tools/disasm/functions.txt | 178 +- tools/disasm/variables.txt | 75 +- undefined_syms.txt | 17 - 21 files changed, 4289 insertions(+), 649 deletions(-) diff --git a/include/z64save.h b/include/z64save.h index 34e8d0c2b3..8478e98801 100644 --- a/include/z64save.h +++ b/include/z64save.h @@ -1587,18 +1587,18 @@ typedef enum { #define EVENTINF_50 0x50 #define EVENTINF_51 0x51 #define EVENTINF_HAS_DAYTIME_TRANSITION_CS 0x52 -#define EVENTINF_53 0x53 -#define EVENTINF_54 0x54 -#define EVENTINF_55 0x55 -// Enabled when Gyorg's intro cutscene has been watched -#define EVENTINF_56 0x56 +// Enabled when various boss intro cutscene has been watched +#define EVENTINF_INTRO_CS_WATCHED_GOHT 0x53 +#define EVENTINF_INTRO_CS_WATCHED_ODOLWA 0x54 +#define EVENTINF_INTRO_CS_WATCHED_TWINMOLD 0x55 +#define EVENTINF_INTRO_CS_WATCHED_GYORG 0x56 +#define EVENTINF_INTRO_CS_WATCHED_IGOS_DU_IKANA 0x57 +#define EVENTINF_INTRO_CS_WATCHED_WART 0x60 +#define EVENTINF_INTRO_CS_WATCHED_MAJORA 0x61 +#define EVENTINF_ENTR_CS_WATCHED_GOHT 0x62 +#define EVENTINF_INTRO_CS_WATCHED_GOMESS 0x63 -#define EVENTINF_57 0x57 -#define EVENTINF_60 0x60 -#define EVENTINF_61 0x61 -#define EVENTINF_62 0x62 -#define EVENTINF_63 0x63 #define EVENTINF_64 0x64 #define EVENTINF_65 0x65 #define EVENTINF_66 0x66 diff --git a/include/z64subs.h b/include/z64subs.h index 2e751dfc45..a6873ff976 100644 --- a/include/z64subs.h +++ b/include/z64subs.h @@ -133,7 +133,7 @@ s32 SubS_CopyPointFromPathCheckBounds(Path* path, s32 pointIndex, Vec3f* dst); s32 SubS_Offer(Actor* actor, struct PlayState* play, f32 xzRange, f32 yRange, s32 itemId, SubSOfferMode mode); void SubS_FillShadowTex(s32 startCol, s32 startRow, u8* tex, s32 size); -void SubS_GenShadowTex(Vec3f bodyPartsPos[], Vec3f* worldPos, u8* tex, f32 tween, u8 bodyPartsNum, u8 sizes[], s8 parentBodyParts[]); +void SubS_GenShadowTex(Vec3f bodyPartsPos[], Vec3f* worldPos, u8* tex, f32 weight, u8 bodyPartsNum, u8 sizes[], s8 parentBodyParts[]); void SubS_DrawShadowTex(Actor* actor, struct GameState* gameState, u8* tex); s16 SubS_ComputeTrackPointRot(s16* rot, s16 rotMax, s16 target, f32 slowness, f32 stepMin, f32 stepMax); diff --git a/spec b/spec index 7e6a30916d..2626fee0df 100644 --- a/spec +++ b/spec @@ -3519,8 +3519,7 @@ beginseg name "ovl_Boss_Hakugin" compress include "build/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.o" - include "build/data/ovl_Boss_Hakugin/ovl_Boss_Hakugin.data.o" - include "build/data/ovl_Boss_Hakugin/ovl_Boss_Hakugin.reloc.o" + include "build/src/overlays/actors/ovl_Boss_Hakugin/ovl_Boss_Hakugin_reloc.o" endseg beginseg diff --git a/src/code/z_parameter.c b/src/code/z_parameter.c index a00341dec4..f292f6d903 100644 --- a/src/code/z_parameter.c +++ b/src/code/z_parameter.c @@ -7218,15 +7218,15 @@ void Interface_Init(PlayState* play) { (play->sceneId != SCENE_LAST_GORON) && (play->sceneId != SCENE_LAST_ZORA) && (play->sceneId != SCENE_LAST_LINK)) { - CLEAR_EVENTINF(EVENTINF_53); // Goht intro cutscene watched - CLEAR_EVENTINF(EVENTINF_54); // Odolwa intro cutscene watched - CLEAR_EVENTINF(EVENTINF_55); // Twinmold intro cutscene watched - CLEAR_EVENTINF(EVENTINF_56); // Gyorg intro cutscene watched - CLEAR_EVENTINF(EVENTINF_57); // Igos du Ikana intro cutscene watched - CLEAR_EVENTINF(EVENTINF_60); // Wart intro cutscene watched - CLEAR_EVENTINF(EVENTINF_61); // Majoras intro cutscene watched - CLEAR_EVENTINF(EVENTINF_62); // - CLEAR_EVENTINF(EVENTINF_63); // Gomess intro cutscene watched + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_GOHT); + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_ODOLWA); + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_TWINMOLD); + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_GYORG); + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_IGOS_DU_IKANA); + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_WART); + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_MAJORA); + CLEAR_EVENTINF(EVENTINF_ENTR_CS_WATCHED_GOHT); + CLEAR_EVENTINF(EVENTINF_INTRO_CS_WATCHED_GOMESS); } sFinalHoursClockDigitsRed = sFinalHoursClockFrameEnvRed = sFinalHoursClockFrameEnvGreen = diff --git a/src/code/z_sub_s.c b/src/code/z_sub_s.c index 322cb62432..cdfd9273a7 100644 --- a/src/code/z_sub_s.c +++ b/src/code/z_sub_s.c @@ -967,7 +967,7 @@ void SubS_FillShadowTex(s32 startCol, s32 startRow, u8* tex, s32 size) { } } -void SubS_GenShadowTex(Vec3f bodyPartsPos[], Vec3f* worldPos, u8* tex, f32 tween, u8 bodyPartsNum, u8 sizes[], +void SubS_GenShadowTex(Vec3f bodyPartsPos[], Vec3f* worldPos, u8* tex, f32 weight, u8 bodyPartsNum, u8 sizes[], s8 parentBodyParts[]) { Vec3f pos; Vec3f startVec; @@ -982,9 +982,9 @@ void SubS_GenShadowTex(Vec3f bodyPartsPos[], Vec3f* worldPos, u8* tex, f32 tween parentBodyPart = parentBodyParts[i]; bodyPartPos = &bodyPartsPos[i]; - pos.x = (bodyPartsPos[parentBodyPart].x - bodyPartPos->x) * tween + (bodyPartPos->x - worldPos->x); - pos.y = (bodyPartsPos[parentBodyPart].y - bodyPartPos->y) * tween + (bodyPartPos->y - worldPos->y); - pos.z = (bodyPartsPos[parentBodyPart].z - bodyPartPos->z) * tween + (bodyPartPos->z - worldPos->z); + pos.x = (bodyPartsPos[parentBodyPart].x - bodyPartPos->x) * weight + (bodyPartPos->x - worldPos->x); + pos.y = (bodyPartsPos[parentBodyPart].y - bodyPartPos->y) * weight + (bodyPartPos->y - worldPos->y); + pos.z = (bodyPartsPos[parentBodyPart].z - bodyPartPos->z) * weight + (bodyPartPos->z - worldPos->z); } else { bodyPartPos = &bodyPartsPos[i]; diff --git a/src/overlays/actors/ovl_Boss_01/z_boss_01.c b/src/overlays/actors/ovl_Boss_01/z_boss_01.c index 50037a4ba5..f0c5d690b8 100644 --- a/src/overlays/actors/ovl_Boss_01/z_boss_01.c +++ b/src/overlays/actors/ovl_Boss_01/z_boss_01.c @@ -934,7 +934,7 @@ void Boss01_Init(Actor* thisx, PlayState* play) { SkelAnime_InitFlex(play, &this->skelAnime, &gOdolwaSkel, &gOdolwaReadyAnim, this->jointTable, this->morphTable, ODOLWA_LIMB_MAX); - if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_54)) { + if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_INTRO_CS_WATCHED_ODOLWA)) { Boss01_SetupWait(this, play, ODOLWA_WAIT_READY); this->actor.gravity = -2.5f; sOdolwaMusicStartTimer = KREG(15) + 20; @@ -1138,7 +1138,7 @@ void Boss01_IntroCutscene(Boss01* this, PlayState* play) { Cutscene_StopManual(play, &play->csCtx); Player_SetCsActionWithHaltedActors(play, &this->actor, PLAYER_CSACTION_END); this->actor.flags |= ACTOR_FLAG_TARGETABLE; - SET_EVENTINF(EVENTINF_54); + SET_EVENTINF(EVENTINF_INTRO_CS_WATCHED_ODOLWA); } break; @@ -3017,6 +3017,7 @@ void Boss01_FillShadowTex(Boss01* this, u8* tex, f32 weight) { Vec3f startVec; for (i = 0; i < ODOLWA_BODYPART_MAX; i++) { + // TODO: match with a continue if ((weight == 0.0f) || (y = sParentShadowBodyParts[i]) > BODYPART_NONE) { if (weight > 0.0f) { VEC3F_LERPIMPDST(&lerp, &this->bodyPartsPos[i], &this->bodyPartsPos[y], weight); diff --git a/src/overlays/actors/ovl_Boss_02/z_boss_02.c b/src/overlays/actors/ovl_Boss_02/z_boss_02.c index 691931fcea..ce92ceb44d 100644 --- a/src/overlays/actors/ovl_Boss_02/z_boss_02.c +++ b/src/overlays/actors/ovl_Boss_02/z_boss_02.c @@ -602,7 +602,7 @@ void Boss02_Init(Actor* thisx, PlayState* play) { this->actor.draw = Boss02_Static_Draw; this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; this->playerScale = 0.01f; - if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_55) || (sBlueWarp != NULL)) { + if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_INTRO_CS_WATCHED_TWINMOLD) || (sBlueWarp != NULL)) { this->unk_1D20 = 0; sTwinmoldMusicStartTimer = KREG(15) + 20; } else { @@ -2230,7 +2230,7 @@ void func_809DEAC4(Boss02* this, PlayState* play) { this->unk_1D20 = 0; sRedTwinmold->unk_0144 = sBlueTwinmold->unk_0144 = 3; sRedTwinmold->unk_0146[0] = sBlueTwinmold->unk_0146[0] = 60; - SET_EVENTINF(EVENTINF_55); + SET_EVENTINF(EVENTINF_INTRO_CS_WATCHED_TWINMOLD); } break; diff --git a/src/overlays/actors/ovl_Boss_03/z_boss_03.c b/src/overlays/actors/ovl_Boss_03/z_boss_03.c index 4bd55fcc6e..9dc32de4e3 100644 --- a/src/overlays/actors/ovl_Boss_03/z_boss_03.c +++ b/src/overlays/actors/ovl_Boss_03/z_boss_03.c @@ -517,8 +517,7 @@ void Boss03_Init(Actor* thisx, PlayState* play2) { GYORG_LIMB_MAX); Actor_SetScale(&this->actor, 0.2f); - // CHECK_EVENTINF(EVENTINF_56): intro cutscene already watched - if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_56)) { + if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_INTRO_CS_WATCHED_GYORG)) { this->actionFunc = func_809E344C; D_809E9842 = false; SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 1); @@ -1377,7 +1376,7 @@ void Boss03_IntroCutscene(Boss03* this, PlayState* play) { func_809E344C(this, play); this->workTimer[WORK_TIMER_UNK1_A] = 50; - SET_EVENTINF(EVENTINF_56); + SET_EVENTINF(EVENTINF_INTRO_CS_WATCHED_GYORG); } break; } diff --git a/src/overlays/actors/ovl_Boss_04/z_boss_04.c b/src/overlays/actors/ovl_Boss_04/z_boss_04.c index ecffb2e681..3c2e437709 100644 --- a/src/overlays/actors/ovl_Boss_04/z_boss_04.c +++ b/src/overlays/actors/ovl_Boss_04/z_boss_04.c @@ -200,7 +200,7 @@ void Boss04_Init(Actor* thisx, PlayState* play2) { this->actor.world.pos.z = this->unk_6F0; Actor_UpdateBgCheckInfo(play, &this->actor, 35.0f, 60.0f, 60.0f, UPDBGCHECKINFO_FLAG_4); - if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_60)) { + if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_INTRO_CS_WATCHED_WART)) { func_809ECD00(this, play); this->actor.world.pos.y = this->actor.floorHeight + 160.0f; phi_f24 = this->actor.floorHeight; @@ -393,7 +393,7 @@ void func_809EC568(Boss04* this, PlayState* play) { Cutscene_StopManual(play, &play->csCtx); Player_SetCsActionWithHaltedActors(play, &this->actor, PLAYER_CSACTION_END); Play_DisableMotionBlur(); - SET_EVENTINF(EVENTINF_60); + SET_EVENTINF(EVENTINF_INTRO_CS_WATCHED_WART); } break; } diff --git a/src/overlays/actors/ovl_Boss_06/z_boss_06.c b/src/overlays/actors/ovl_Boss_06/z_boss_06.c index 544dada9cb..8065b220cd 100644 --- a/src/overlays/actors/ovl_Boss_06/z_boss_06.c +++ b/src/overlays/actors/ovl_Boss_06/z_boss_06.c @@ -136,7 +136,7 @@ void Boss06_Init(Actor* thisx, PlayState* play) { D_809F4970 = (EnKnight*)this->actor.parent; this->actor.colChkInfo.damageTable = &sDamageTable; - if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_57)) { + if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_INTRO_CS_WATCHED_IGOS_DU_IKANA)) { this->actionFunc = func_809F2E14; } else { this->actionFunc = func_809F2B64; diff --git a/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c b/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c index c02184a4cd..455bba719b 100644 --- a/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c +++ b/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c @@ -2,38 +2,105 @@ * File: z_boss_hakugin.c * Overlay: ovl_Boss_Hakugin * Description: Goht + * + * This actor is responsible for handling most things involving the Goht boss fight. In addition to Goht itself, this + * also includes the block of ice it is encased in before the fight begins, the lightning and electric ball attacks it + * can fire at the player, the various cutscenes that play during the fight, etc. */ #include "z_boss_hakugin.h" #include "z64quake.h" #include "z64rumble.h" +#include "overlays/actors/ovl_En_Bom/z_en_bom.h" +#include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" +#include "overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.h" +#include "overlays/actors/ovl_En_Hakurock/z_en_hakurock.h" +#include "overlays/actors/ovl_Item_B_Heart/z_item_b_heart.h" +#include "overlays/effects/ovl_Effect_Ss_Hitmark/z_eff_ss_hitmark.h" + +#include "objects/gameplay_keep/gameplay_keep.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_UNFRIENDLY | ACTOR_FLAG_10 | ACTOR_FLAG_20) #define THIS ((BossHakugin*)thisx) -void BossHakugin_Init(Actor* thisx, PlayState* play); +#define GOHT_LIMB_DRAW_FLAG(limbIndex) (1 << ((limbIndex)-1)) + +void BossHakugin_Init(Actor* thisx, PlayState* play2); void BossHakugin_Destroy(Actor* thisx, PlayState* play); void BossHakugin_Update(Actor* thisx, PlayState* play); void BossHakugin_Draw(Actor* thisx, PlayState* play); -void func_80B08018(BossHakugin* this, PlayState* play); -void func_80B0813C(BossHakugin* this, PlayState* play); -void func_80B082AC(BossHakugin* this, PlayState* play); -void func_80B0863C(BossHakugin* this, PlayState* play); -void func_80B08960(BossHakugin* this, PlayState* play); -void func_80B08CB8(BossHakugin* this, PlayState* play); -void func_80B091D8(BossHakugin* this, PlayState* play); -void func_80B094E0(BossHakugin* this, PlayState* play); -void func_80B098BC(BossHakugin* this, PlayState* play); -void func_80B09C78(BossHakugin* this, PlayState* play); -void func_80B09E20(BossHakugin* this, PlayState* play); -void func_80B09EDC(BossHakugin* this, PlayState* play); -void func_80B0A2A4(BossHakugin* this, PlayState* play); -void func_80B0A638(BossHakugin* this, PlayState* play); -void func_80B0A8C4(BossHakugin* this, PlayState* play); +void BossHakugin_SpawnLargeStalactiteWalls(BossHakugin* this); +void BossHakugin_SpawnBlueWarpAndHeartContainer(BossHakugin* this, PlayState* play); +void BossHakugin_EntranceCutscene(BossHakugin* this, PlayState* play); +void BossHakugin_SetupFrozenBeforeFight(BossHakugin* this); +void BossHakugin_FrozenBeforeFight(BossHakugin* this, PlayState* play); +void BossHakugin_IntroCutsceneThaw(BossHakugin* this, PlayState* play); +void BossHakugin_SetupIntroCutsceneWakeUp(BossHakugin* this, PlayState* play); +void BossHakugin_IntroCutsceneWakeUp(BossHakugin* this, PlayState* play); +void BossHakugin_SetupIntroCutsceneRun(BossHakugin* this, PlayState* play); +void BossHakugin_IntroCutsceneRun(BossHakugin* this, PlayState* play); +void BossHakugin_SetupRun(BossHakugin* this); +void BossHakugin_Run(BossHakugin* this, PlayState* play); +void BossHakugin_SetupCharge(BossHakugin* this); +void BossHakugin_Charge(BossHakugin* this, PlayState* play); +void BossHakugin_Downed(BossHakugin* this, PlayState* play); +void BossHakugin_Throw(BossHakugin* this, PlayState* play); +void BossHakugin_SetupWait(BossHakugin* this, PlayState* play); +void BossHakugin_Wait(BossHakugin* this, PlayState* play); +void BossHakugin_SetupShootLightning(BossHakugin* this); +void BossHakugin_ShootLightning(BossHakugin* this, PlayState* play); +void BossHakugin_SetupCutsceneStart(BossHakugin* this); +void BossHakugin_CutsceneStart(BossHakugin* this, PlayState* play); +void BossHakugin_SetupDeathCutsceneRun(BossHakugin* this); +void BossHakugin_DeathCutsceneRun(BossHakugin* this, PlayState* play); +void BossHakugin_SetupDeathCutsceneSwerveIntoWall(BossHakugin* this); +void BossHakugin_DeathCutsceneSwerveIntoWall(BossHakugin* this, PlayState* play); +void BossHakugin_SetupDeathCutsceneCrushedByRocks(BossHakugin* this); +void BossHakugin_DeathCutsceneCrushedByRocks(BossHakugin* this, PlayState* play); +void BossHakugin_GenShadowTex(u8* tex, BossHakugin* this); +void BossHakugin_DrawShadowTex(u8* tex, BossHakugin* this, PlayState* play); +void BossHakugin_SpawnCrushingRocks(BossHakugin* this); +void BossHakugin_UpdateCrushingRocksCollision(BossHakugin* this); +void BossHakugin_UpdateStationaryCrushingRocks(Actor* thisx, PlayState* play2); +void BossHakugin_DrawCrushingRocks(Actor* thisx, PlayState* play); + +typedef enum GohtElectricBallState { + // The electric ball is not active. Goht will start charging the electric ball the next time the player hits it with + // Goron Spikes while it's running. + /* 0 */ GOHT_ELECTRIC_BALL_STATE_NONE, + + // Goht is charging the electric ball up and will shoot it forward after a short delay. + /* 1 */ GOHT_ELECTRIC_BALL_STATE_CHARGE, + + // The electric ball is flying forward from Goht's head and slowing down until its speed reaches 50.0f. + /* 2 */ GOHT_ELECTRIC_BALL_STATE_FLY_FORWARD, + + // The electric ball is rotating to track the player and gradually moves towards them. + /* 3 */ GOHT_ELECTRIC_BALL_STATE_FLY_TOWARDS_PLAYER, + + // The electric ball hit the player or the environment, so it's fading away. It can't hurt the player in this state. + /* 4 */ GOHT_ELECTRIC_BALL_STATE_FADE_OUT +} GohtElectricBallState; + +typedef enum GohtDirection { + /* -1 */ GOHT_DIRECTION_COUNTERCLOCKWISE = -1, + /* 1 */ GOHT_DIRECTION_CLOCKWISE = 1 +} GohtDirection; + +typedef enum GohtNextCutsceneType { + /* 0 */ GOHT_NEXT_CUTSCENE_TYPE_ENTRANCE, + /* 1 */ GOHT_NEXT_CUTSCENE_TYPE_INTRO +} GohtNextCutsceneType; + +typedef enum GohtShadowSize { + /* 0 */ GOHT_SHADOW_SIZE_MEDIUM, + /* 1 */ GOHT_SHADOW_SIZE_LARGE, + /* 2 */ GOHT_SHADOW_SIZE_EXTRA_LARGE, + /* 3 */ GOHT_SHADOW_SIZE_SMALL +} GohtShadowSize; -#if 0 ActorInit Boss_Hakugin_InitVars = { /**/ ACTOR_BOSS_HAKUGIN, /**/ ACTORCAT_BOSS, @@ -46,342 +113,3729 @@ ActorInit Boss_Hakugin_InitVars = { /**/ BossHakugin_Draw, }; -// static ColliderJntSphElementInit sJntSphElementsInit[19] = { -static ColliderJntSphElementInit D_80B0E700[19] = { +static ColliderJntSphElementInit sJntSphElementsInit[GOHT_COLLIDER_BODYPART_MAX] = { { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x00 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NONE, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 16, { { 1300, 200, 0 }, 30 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x00 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NONE, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_HEAD, { { 1300, 200, 0 }, 30 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 15, { { 1400, 300, 0 }, 35 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_THORAX, { { 1400, 300, 0 }, 35 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 2, { { 1600, -400, 0 }, 26 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_PELVIS, { { 1600, -400, 0 }, 26 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 6, { { 1700, 200, 400 }, 18 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_FRONT_RIGHT_UPPER_LEG, { { 1700, 200, 400 }, 18 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 8, { { 1000, 0, 0 }, 10 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_FRONT_RIGHT_LOWER_LEG, { { 1000, 0, 0 }, 10 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 9, { { 500, 0, 0 }, 9 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_FRONT_RIGHT_HOOF, { { 500, 0, 0 }, 9 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 11, { { 1700, 200, -400 }, 18 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_FRONT_LEFT_UPPER_LEG, { { 1700, 200, -400 }, 18 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 13, { { 1000, 0, 0 }, 10 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_FRONT_LEFT_LOWER_LEG, { { 1000, 0, 0 }, 10 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 14, { { 500, 0, 0 }, 9 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_FRONT_LEFT_HOOF, { { 500, 0, 0 }, 9 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 21, { { 800, 0, 400 }, 15 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_RIGHT_THIGH, { { 800, 0, 400 }, 15 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 22, { { 500, 200, 0 }, 9 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_RIGHT_SHIN, { { 500, 200, 0 }, 9 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 24, { { 300, 0, 0 }, 8 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_RIGHT_PASTERN, { { 300, 0, 0 }, 8 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 25, { { 500, 100, 0 }, 9 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_RIGHT_HOOF, { { 500, 100, 0 }, 9 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 28, { { 800, 0, -400 }, 15 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_LEFT_THIGH, { { 800, 0, -400 }, 15 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 29, { { 500, 200, 0 }, 9 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_LEFT_SHIN, { { 500, 200, 0 }, 9 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 31, { { 300, 0, 0 }, 8 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_LEFT_PASTERN, { { 300, 0, 0 }, 8 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 32, { { 500, 100, 0 }, 9 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_BACK_LEFT_HOOF, { { 500, 100, 0 }, 9 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 16, { { 2400, -400, -2900 }, 12 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_HEAD, { { 2400, -400, -2900 }, 12 }, 270 }, }, { - { ELEMTYPE_UNK2, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NORMAL, BUMP_ON | BUMP_NO_HITMARK, OCELEM_ON, }, - { 16, { { 2400, -400, 2900 }, 12 }, 270 }, + { + ELEMTYPE_UNK2, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NORMAL, + BUMP_ON | BUMP_NO_HITMARK, + OCELEM_ON, + }, + { GOHT_LIMB_HEAD, { { 2400, -400, 2900 }, 12 }, 270 }, }, }; -// static ColliderJntSphInit sJntSphInit = { -static ColliderJntSphInit D_80B0E9AC = { - { COLTYPE_METAL, AT_ON | AT_TYPE_ENEMY, AC_ON | AC_HARD | AC_TYPE_PLAYER, OC1_ON | OC1_TYPE_ALL, OC2_TYPE_1, COLSHAPE_JNTSPH, }, - ARRAY_COUNT(sJntSphElementsInit), D_80B0E700, // sJntSphElementsInit, +static ColliderJntSphInit sJntSphInit = { + { + COLTYPE_METAL, + AT_ON | AT_TYPE_ENEMY, + AC_ON | AC_HARD | AC_TYPE_PLAYER, + OC1_ON | OC1_TYPE_ALL, + OC2_TYPE_1, + COLSHAPE_JNTSPH, + }, + ARRAY_COUNT(sJntSphElementsInit), + sJntSphElementsInit, }; -// static ColliderTrisElementInit sTrisElementsInit[1] = { -static ColliderTrisElementInit D_80B0E9BC[1] = { +static ColliderTrisElementInit sTrisElementsInit[1] = { { - { ELEMTYPE_UNK5, { 0x20000000, 0x03, 0x08 }, { 0x00000000, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NONE, BUMP_NONE, OCELEM_NONE, }, + { + ELEMTYPE_UNK5, + { 0x20000000, 0x03, 0x08 }, + { 0x00000000, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NONE, + BUMP_NONE, + OCELEM_NONE, + }, { { { 22.0f, 0.0f, 100.0f }, { 0.0f, 0.0f, -100.0f }, { -22.0f, 0.0f, 100.0f } } }, }, }; -// static ColliderTrisInit sTrisInit = { -static ColliderTrisInit D_80B0E9F8 = { - { COLTYPE_NONE, AT_ON | AT_TYPE_ENEMY, AC_NONE, OC1_NONE, OC2_TYPE_1, COLSHAPE_TRIS, }, - ARRAY_COUNT(sTrisElementsInit), D_80B0E9BC, // sTrisElementsInit, +static ColliderTrisInit sTrisInit = { + { + COLTYPE_NONE, + AT_ON | AT_TYPE_ENEMY, + AC_NONE, + OC1_NONE, + OC2_TYPE_1, + COLSHAPE_TRIS, + }, + ARRAY_COUNT(sTrisElementsInit), + sTrisElementsInit, }; -// static ColliderSphereInit sSphereInit = { -static ColliderSphereInit D_80B0EA08 = { - { COLTYPE_NONE, AT_ON | AT_TYPE_ENEMY, AC_NONE, OC1_NONE, OC2_TYPE_2, COLSHAPE_SPHERE, }, - { ELEMTYPE_UNK5, { 0x20000000, 0x00, 0x08 }, { 0x00000000, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_HARD, BUMP_NONE, OCELEM_NONE, }, +static ColliderSphereInit sSphereInit = { + { + COLTYPE_NONE, + AT_ON | AT_TYPE_ENEMY, + AC_NONE, + OC1_NONE, + OC2_TYPE_2, + COLSHAPE_SPHERE, + }, + { + ELEMTYPE_UNK5, + { 0x20000000, 0x00, 0x08 }, + { 0x00000000, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_HARD, + BUMP_NONE, + OCELEM_NONE, + }, { 1, { { 0, 0, 0 }, 23 }, 100 }, }; -// static ColliderCylinderInit sCylinderInit = { -static ColliderCylinderInit D_80B0EA34 = { - { COLTYPE_HARD, AT_ON | AT_TYPE_ENEMY, AC_ON | AC_HARD | AC_TYPE_PLAYER, OC1_ON | OC1_TYPE_ALL, OC2_TYPE_1, COLSHAPE_CYLINDER, }, - { ELEMTYPE_UNK0, { 0xF7CFFFFF, 0x02, 0x00 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_NONE, BUMP_ON, OCELEM_ON, }, +static ColliderCylinderInit sCylinderInit = { + { + COLTYPE_HARD, + AT_ON | AT_TYPE_ENEMY, + AC_ON | AC_HARD | AC_TYPE_PLAYER, + OC1_ON | OC1_TYPE_ALL, + OC2_TYPE_1, + COLSHAPE_CYLINDER, + }, + { + ELEMTYPE_UNK0, + { 0xF7CFFFFF, 0x02, 0x00 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_NONE, + BUMP_ON, + OCELEM_ON, + }, { 170, 40, 0, { 0, 0, 0 } }, }; -// static DamageTable sDamageTable = { -static DamageTable D_80B0EA60 = { - /* Deku Nut */ DMG_ENTRY(0, 0x0), - /* Deku Stick */ DMG_ENTRY(1, 0x0), - /* Horse trample */ DMG_ENTRY(1, 0x0), - /* Explosives */ DMG_ENTRY(1, 0xC), - /* Zora boomerang */ DMG_ENTRY(1, 0x0), - /* Normal arrow */ DMG_ENTRY(1, 0xE), - /* UNK_DMG_0x06 */ DMG_ENTRY(0, 0x0), - /* Hookshot */ DMG_ENTRY(1, 0x0), - /* Goron punch */ DMG_ENTRY(1, 0x0), - /* Sword */ DMG_ENTRY(1, 0x0), - /* Goron pound */ DMG_ENTRY(1, 0x0), - /* Fire arrow */ DMG_ENTRY(1, 0x2), - /* Ice arrow */ DMG_ENTRY(1, 0x3), - /* Light arrow */ DMG_ENTRY(1, 0x4), - /* Goron spikes */ DMG_ENTRY(1, 0xF), - /* Deku spin */ DMG_ENTRY(1, 0x0), - /* Deku bubble */ DMG_ENTRY(1, 0x0), - /* Deku launch */ DMG_ENTRY(1, 0x0), - /* UNK_DMG_0x12 */ DMG_ENTRY(0, 0x0), - /* Zora barrier */ DMG_ENTRY(0, 0x0), - /* Normal shield */ DMG_ENTRY(0, 0x0), - /* Light ray */ DMG_ENTRY(0, 0x0), - /* Thrown object */ DMG_ENTRY(1, 0x0), - /* Zora punch */ DMG_ENTRY(1, 0x0), - /* Spin attack */ DMG_ENTRY(1, 0x0), - /* Sword beam */ DMG_ENTRY(1, 0xD), - /* Normal Roll */ DMG_ENTRY(0, 0x0), - /* UNK_DMG_0x1B */ DMG_ENTRY(0, 0x0), - /* UNK_DMG_0x1C */ DMG_ENTRY(0, 0x0), - /* Unblockable */ DMG_ENTRY(0, 0x0), - /* UNK_DMG_0x1E */ DMG_ENTRY(0, 0x0), - /* Powder Keg */ DMG_ENTRY(1, 0xC), +typedef enum GohtDamageEffect { + // If an attack with this damage effect hits Goht while it's standing upright or running, it will deal no damage and + // spawn some spark effects. If it hits Goht while it's downed, however, it will deal damage with no special effect. + /* 0x0 */ GOHT_DMGEFF_DOWNED_ONLY, + + // Deals damage and surrounds Goht with fire. + /* 0x2 */ GOHT_DMGEFF_FIRE = 2, + + // Deals damage and surrounds Goht with ice that instantly shatters. + /* 0x3 */ GOHT_DMGEFF_FREEZE, + + // Deals damage and surrounds Goht with yellow light orbs. + /* 0x4 */ GOHT_DMGEFF_LIGHT_ORB, + + // Deals damage and can additionally knock Goht down if it hits Goht on the head, thorax, pelvis, or upper legs + // while it isn't charging. + /* 0xC */ GOHT_DMGEFF_EXPLOSIVE = 0xC, + + // Deals damage and surrounds Goht with blue light orbs. + /* 0xD */ GOHT_DMGEFF_BLUE_LIGHT_ORB, + + // Deals damage with no special effect. + /* 0xE */ GOHT_DMGEFF_NONE, + + // Deals damage and can additionally knock Goht down in the same manner as GOHT_DMGEFF_EXPLOSIVE. If Goht is hit by + // an attack with this damage effect and it does *not* knock it down, however, then Goht will charge up and fire an + // electric ball attack. + /* 0xF */ GOHT_DMGEFF_GORON_SPIKES +} GohtDamageEffect; + +static DamageTable sDamageTable = { + /* Deku Nut */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Deku Stick */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Horse trample */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Explosives */ DMG_ENTRY(1, GOHT_DMGEFF_EXPLOSIVE), + /* Zora boomerang */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Normal arrow */ DMG_ENTRY(1, GOHT_DMGEFF_NONE), + /* UNK_DMG_0x06 */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Hookshot */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Goron punch */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Sword */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Goron pound */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Fire arrow */ DMG_ENTRY(1, GOHT_DMGEFF_FIRE), + /* Ice arrow */ DMG_ENTRY(1, GOHT_DMGEFF_FREEZE), + /* Light arrow */ DMG_ENTRY(1, GOHT_DMGEFF_LIGHT_ORB), + /* Goron spikes */ DMG_ENTRY(1, GOHT_DMGEFF_GORON_SPIKES), + /* Deku spin */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Deku bubble */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Deku launch */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* UNK_DMG_0x12 */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Zora barrier */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Normal shield */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Light ray */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Thrown object */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Zora punch */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Spin attack */ DMG_ENTRY(1, GOHT_DMGEFF_DOWNED_ONLY), + /* Sword beam */ DMG_ENTRY(1, GOHT_DMGEFF_BLUE_LIGHT_ORB), + /* Normal Roll */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* UNK_DMG_0x1B */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* UNK_DMG_0x1C */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Unblockable */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* UNK_DMG_0x1E */ DMG_ENTRY(0, GOHT_DMGEFF_DOWNED_ONLY), + /* Powder Keg */ DMG_ENTRY(1, GOHT_DMGEFF_EXPLOSIVE), }; -// sColChkInfoInit -static CollisionCheckInfoInit D_80B0EA80 = { 30, 80, 100, MASS_IMMOVABLE }; +static CollisionCheckInfoInit sColChkInfoInit = { 30, 80, 100, MASS_IMMOVABLE }; -// static InitChainEntry sInitChain[] = { -static InitChainEntry D_80B0EAD8[] = { - ICHAIN_S8(hintId, 27, ICHAIN_CONTINUE), - ICHAIN_VEC3F_DIV1000(scale, 27, ICHAIN_CONTINUE), - ICHAIN_U8(targetMode, TARGET_MODE_5, ICHAIN_CONTINUE), - ICHAIN_F32_DIV1000(gravity, -2000, ICHAIN_STOP), +/** + * For whatever reason, the chin of the "face" on Goht's head relies on the actor to put the texture in segment 0x08 to + * render properly. Since the texture never changes, the reason for doing it like this is unclear. + */ +TexturePtr sChinTexture = gGohtMetalPlateWithCirclePatternTex; + +static s8 sLimbToBodyParts[GOHT_LIMB_MAX] = { + BODYPART_NONE, // GOHT_LIMB_NONE + BODYPART_NONE, // GOHT_LIMB_ROOT + GOHT_BODYPART_PELVIS, // GOHT_LIMB_PELVIS + BODYPART_NONE, // GOHT_LIMB_THORAX_ROOT + BODYPART_NONE, // GOHT_LIMB_THORAX_WRAPPER + BODYPART_NONE, // GOHT_LIMB_FRONT_RIGHT_LEG_ROOT + GOHT_BODYPART_FRONT_RIGHT_UPPER_LEG, // GOHT_LIMB_FRONT_RIGHT_UPPER_LEG + BODYPART_NONE, // GOHT_LIMB_FRONT_RIGHT_LOWER_LEG_ROOT + GOHT_BODYPART_FRONT_RIGHT_LOWER_LEG, // GOHT_LIMB_FRONT_RIGHT_LOWER_LEG + GOHT_BODYPART_FRONT_RIGHT_HOOF, // GOHT_LIMB_FRONT_RIGHT_HOOF + BODYPART_NONE, // GOHT_LIMB_FRONT_LEFT_LEG_ROOT + GOHT_BODYPART_FRONT_LEFT_UPPER_LEG, // GOHT_LIMB_FRONT_LEFT_UPPER_LEG + BODYPART_NONE, // GOHT_LIMB_FRONT_LEFT_LOWER_LEG_ROOT + GOHT_BODYPART_FRONT_LEFT_LOWER_LEG, // GOHT_LIMB_FRONT_LEFT_LOWER_LEG + GOHT_BODYPART_FRONT_LEFT_HOOF, // GOHT_LIMB_FRONT_LEFT_HOOF + GOHT_BODYPART_THORAX, // GOHT_LIMB_THORAX + GOHT_BODYPART_HEAD, // GOHT_LIMB_HEAD + BODYPART_NONE, // GOHT_LIMB_JAW_ROOT + BODYPART_NONE, // GOHT_LIMB_JAW + BODYPART_NONE, // GOHT_LIMB_BACK_RIGHT_LEG_ROOT + BODYPART_NONE, // GOHT_LIMB_BACK_RIGHT_LEG_WRAPPER + GOHT_BODYPART_BACK_RIGHT_THIGH, // GOHT_LIMB_BACK_RIGHT_THIGH + GOHT_BODYPART_BACK_RIGHT_SHIN, // GOHT_LIMB_BACK_RIGHT_SHIN + BODYPART_NONE, // GOHT_LIMB_BACK_RIGHT_PASTERN_ROOT + BODYPART_NONE, // GOHT_LIMB_BACK_RIGHT_PASTERN + GOHT_BODYPART_BACK_RIGHT_HOOF, // GOHT_LIMB_BACK_RIGHT_HOOF + BODYPART_NONE, // GOHT_LIMB_BACK_LEFT_LEG_ROOT + BODYPART_NONE, // GOHT_LIMB_BACK_LEFT_LEG_WRAPPER + GOHT_BODYPART_BACK_LEFT_THIGH, // GOHT_LIMB_BACK_LEFT_THIGH + GOHT_BODYPART_BACK_LEFT_SHIN, // GOHT_LIMB_BACK_LEFT_SHIN + BODYPART_NONE, // GOHT_LIMB_BACK_LEFT_PASTERN_ROOT + BODYPART_NONE, // GOHT_LIMB_BACK_LEFT_PASTERN + GOHT_BODYPART_BACK_LEFT_HOOF, // GOHT_LIMB_BACK_LEFT_HOOF }; -#endif - -extern ColliderJntSphElementInit D_80B0E700[19]; -extern ColliderJntSphInit D_80B0E9AC; -extern ColliderTrisElementInit D_80B0E9BC[1]; -extern ColliderTrisInit D_80B0E9F8; -extern ColliderSphereInit D_80B0EA08; -extern ColliderCylinderInit D_80B0EA34; -extern DamageTable D_80B0EA60; -extern CollisionCheckInfoInit D_80B0EA80; -extern InitChainEntry D_80B0EAD8[]; - -extern UNK_TYPE D_06002054; -extern UNK_TYPE D_0600319C; -extern UNK_TYPE D_06010488; -extern UNK_TYPE D_06011208; -extern UNK_TYPE D_06012ED0; -extern UNK_TYPE D_060134D0; -extern UNK_TYPE D_06013828; -extern UNK_TYPE D_06014040; - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/BossHakugin_Init.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/BossHakugin_Destroy.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0573C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B057A4.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B058C0.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B05A64.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B05B04.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B05CBC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B05D4C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B05EE0.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0607C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B06558.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B06600.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0696C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B06B20.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B06C08.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B06D38.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B06F48.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0728C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B07450.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B07700.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0791C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B07B88.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B07BFC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B07DA4.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B07EEC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B08018.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B08124.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0813C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0825C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B082AC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B08550.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0863C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B08848.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B08960.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B08C1C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B08CB8.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09178.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B091D8.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B093C0.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B094E0.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09840.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B098BC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09A94.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09C78.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09DFC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09E20.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09E84.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09EDC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B09F7C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0A2A4.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0A5EC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0A638.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0A87C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0A8C4.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0AC30.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0ADFC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0B238.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0B34C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0B3F4.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0B548.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0B660.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/BossHakugin_Update.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0C000.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0C1BC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0C398.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0C570.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0C7B0.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0CAF0.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0CCD8.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0CF24.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/BossHakugin_Draw.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0D2B8.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0D69C.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0D750.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0D9CC.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0DFA8.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0E548.s") - -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_Boss_Hakugin/func_80B0E5A4.s") +/** + * Each type of malfunction effect is tied to a specific limb, and this array can be used to convert between the + * malfunction effect type and the limb with which it's associated. + */ +s32 sMalfunctionTypeToLimbIndex[GOHT_MALFUNCTION_NUM_TYPES] = { + GOHT_LIMB_BACK_LEFT_THIGH, // FHGFLASH_SHOCK_GOHT_BACK_LEFT_THIGH + GOHT_LIMB_BACK_RIGHT_PASTERN, // FHGFLASH_SHOCK_GOHT_BACK_RIGHT_PASTERN + GOHT_LIMB_JAW_ROOT, // FHGFLASH_SHOCK_GOHT_JAW_ROOT + GOHT_LIMB_FRONT_LEFT_LOWER_LEG, // FHGFLASH_SHOCK_GOHT_FRONT_LEFT_LOWER_LEG + GOHT_LIMB_FRONT_RIGHT_UPPER_LEG, // FHGFLASH_SHOCK_GOHT_FRONT_RIGHT_UPPER_LEG +}; + +/** + * These colors are used for the sparkles that appear around the block of ice when Goht is frozen before the fight. + */ +static Color_RGBA8 sSparklePrimColor = { 250, 250, 250, 255 }; +static Color_RGBA8 sSparkleEnvColor = { 180, 180, 180, 255 }; + +/** + * This is used for the dynamic light that can light up the arena when various effects are active, the light orb that + * appears near Goht's head when it's charging up an attack, and the light orbs that make up the electric ball attack. + */ +static Color_RGB8 sLightColor = { 0, 150, 255 }; + +/** + * This is used for both the lightning that appears near Goht's head when it's charging up an attack and the lightning + * segments that Goht fires to attack the player. + */ +static Color_RGB8 sLightningColor = { 0, 255, 255 }; + +void BossHakugin_Init(Actor* thisx, PlayState* play2) { + static s32 sTexturesDesegmented = false; + static InitChainEntry sInitChain[] = { + ICHAIN_S8(hintId, TATL_HINT_ID_GOHT, ICHAIN_CONTINUE), + ICHAIN_VEC3F_DIV1000(scale, 27, ICHAIN_CONTINUE), + ICHAIN_U8(targetMode, TARGET_MODE_5, ICHAIN_CONTINUE), + ICHAIN_F32_DIV1000(gravity, -2000, ICHAIN_STOP), + }; + PlayState* play = play2; + BossHakugin* this = THIS; + Actor** actorPtr; + s32 pad; + s32 i; + + Actor_ProcessInitChain(&this->actor, sInitChain); + SkelAnime_InitFlex(play, &this->skelAnime, &gGohtSkel, &gGohtRunAnim, this->jointTable, this->morphTable, + GOHT_LIMB_MAX); + + if (!sTexturesDesegmented) { + sTexturesDesegmented = true; + sChinTexture = Lib_SegmentedToVirtual(sChinTexture); + } + + Collider_InitAndSetJntSph(play, &this->bodyCollider, &this->actor, &sJntSphInit, this->bodyColliderElements); + for (i = 0; i < GOHT_LIGHTNING_SEGMENT_COUNT; i++) { + Collider_InitAndSetTris(play, &this->lightningSegments[i].collider, &this->actor, &sTrisInit, + this->lightningSegments[i].colliderElements); + } + + Collider_InitAndSetSphere(play, &this->electricBallCollider, &this->actor, &sSphereInit); + this->electricBallCollider.dim.worldSphere.radius = 40; + Collider_InitAndSetCylinder(play, &this->iceCollider, &this->actor, &sCylinderInit); + this->iceCollider.dim.pos.x = this->actor.world.pos.x - 50.0f; + this->iceCollider.dim.pos.y = this->actor.world.pos.y; + this->iceCollider.dim.pos.z = this->actor.world.pos.z; + this->iceCollider.dim.radius = 150; + this->iceCollider.dim.height = 300; + this->lightNode = LightContext_InsertLight(play, &play->lightCtx, &this->lightInfo); + Lights_PointNoGlowSetInfo(&this->lightInfo, 0, 0, 0, 255, 255, 180, -1); + this->direction = GOHT_DIRECTION_COUNTERCLOCKWISE; + + if (CHECK_WEEKEVENTREG(WEEKEVENTREG_CLEARED_SNOWHEAD_TEMPLE)) { + BossHakugin_SpawnBlueWarpAndHeartContainer(this, play); + Actor_Kill(&this->actor); + return; + } + + for (i = 0; i < ARRAY_COUNT(this->boulders); i++) { + //! FAKE: + actorPtr = &this->boulders[i]; + *actorPtr = Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_HAKUROCK, 0.0f, 0.0f, 0.0f, 0, 0, + 0, EN_HAKUROCK_TYPE_BOULDER); + } + + for (i = 0; i < ARRAY_COUNT(this->stalactites); i++) { + this->stalactites[i] = Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_HAKUROCK, 0.0f, 0.0f, + 0.0f, 0, 0, 0, EN_HAKUROCK_TYPE_FALLING_STALACTITE); + } + + for (i = 0; i < GOHT_ROCK_EFFECT_COUNT; i++) { + this->rockEffects[i].timer = -1; + } + + CollisionCheck_SetInfo(&this->actor.colChkInfo, &sDamageTable, &sColChkInfoInit); + this->baseRotY = this->actor.shape.rot.y; + this->limbDrawFlags = 0xFFFFFFFF; + + if (CHECK_EVENTINF(EVENTINF_INTRO_CS_WATCHED_GOHT)) { + SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 0, NA_BGM_BOSS | SEQ_FLAG_ASYNC); + this->actor.world.pos.x = 0.0f; + BossHakugin_SetupRun(this); + } else if (CHECK_EVENTINF(EVENTINF_ENTR_CS_WATCHED_GOHT)) { + this->iceAlpha = 255; + this->iceScaleY = 2.7f; + BossHakugin_SpawnLargeStalactiteWalls(this); + Animation_PlayOnce(&this->skelAnime, &gGohtThawAndBreakWallAnim); + this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; + BossHakugin_SetupFrozenBeforeFight(this); + } else { + this->iceAlpha = 255; + this->iceScaleY = 2.7f; + BossHakugin_SpawnLargeStalactiteWalls(this); + BossHakugin_SetupCutsceneStart(this); + } +} + +void BossHakugin_Destroy(Actor* thisx, PlayState* play) { + BossHakugin* this = THIS; + s32 pad; + s32 i; + + LightContext_RemoveLight(play, &play->lightCtx, this->lightNode); + Collider_DestroyJntSph(play, &this->bodyCollider); + Collider_DestroyCylinder(play, &this->iceCollider); + + for (i = 0; i < GOHT_LIGHTNING_SEGMENT_COUNT; i++) { + Collider_DestroyTris(play, &this->lightningSegments[i].collider); + } + + Collider_DestroySphere(play, &this->electricBallCollider); + AudioSfx_StopByPos(&this->sfxPos); + AudioSfx_StopByPos(&this->electricBallSfxPos); +} + +s32 BossHakugin_Vec3fNormalize(Vec3f* vec) { + f32 magnitude = Math3D_Vec3fMagnitude(vec); + + if (magnitude < 0.0001f) { + return false; + } + + Math_Vec3f_Scale(vec, 1.0f / magnitude); + return true; +} + +/** + * The intention behind this function is to call it repeatedly to gradually rotate `norm` to match the `targetNorm`. + * Specifically, this function rotates `norm` around the axis perpendicular to both `norm` and `targetNorm` (in other + * words, it rotates around the cross product). The amount by which to rotate `norm` per call is determined by + * `angleStep`; if rotating `norm` by this amount would cause it to overshoot the `targetNorm`, then the rotation will + * be clamped such that the `norm` will reach its target exactly. + * + * This function assumes that both `norm` and `targetNorm` are normalized unit vectors; if non-unit vectors are + * supplied, then this function will do nothing at all if the dot product of the two non-unit vectors is positive. It is + * also possible for this function to undershoot the `targetNorm` if the angle between the `norm` and the `targetNorm` + * is less than one degree; in this case, this function will return without doing any rotation. + */ +void BossHakugin_StepVector(Vec3f* norm, Vec3f* targetNorm, f32 angleStep) { + Vec3f crossProduct; + Vec3f result; + f32 dotProduct = (norm->x * targetNorm->x) + (norm->y * targetNorm->y) + (norm->z * targetNorm->z); + f32 maxAngle; + + if (fabsf(dotProduct) < 1.0f) { + maxAngle = Math_FAcosF(dotProduct); + } else if (dotProduct >= 1.0f) { + // Either the dot product is exactly 1.0f, or a non-unit vector was passed into this function. + maxAngle = 0.0f; + } else { + // Either the dot product is exactly -1.0f, or a non-unit vector was passed into this function. + maxAngle = M_PI; + } + + angleStep = CLAMP_MAX(angleStep, maxAngle); + if (angleStep < DEG_TO_RAD(1)) { + return; + } + + Math3D_CrossProduct(norm, targetNorm, &crossProduct); + + if (BossHakugin_Vec3fNormalize(&crossProduct)) { + Matrix_RotateAxisF(angleStep, &crossProduct, MTXMODE_NEW); + Matrix_MultVec3f(norm, &result); + Math_Vec3f_Copy(norm, &result); + } +} + +/** + * Sets up the walls made of large stalactites that confine the player to the part of the boss room near the door; these + * are the stalactites that are destroyed when Goht is thawed just before the fight begins. + */ +void BossHakugin_SpawnLargeStalactiteWalls(BossHakugin* this) { + s32 i; + Actor* last; + Actor* now = this->stalactites[0]; + + now->params = EN_HAKUROCK_TYPE_LARGE_STALACTITE; + now->world.pos.x = -500.0f; + now->world.pos.y = this->actor.world.pos.y; + now->world.pos.z = -3800.0f / 3.0f; + now->shape.rot.y = this->actor.shape.rot.y + 0x4000; + + // Spawns the six large stalactites in front of Goht + for (i = 1; i < 6; i++) { + last = now; + now = this->stalactites[i]; + + now->params = EN_HAKUROCK_TYPE_LARGE_STALACTITE; + now->world.pos.x = last->world.pos.x; + now->world.pos.y = this->actor.world.pos.y; + now->world.pos.z = last->world.pos.z - 133.333333f; // 4000 / 3 + now->shape.rot.y = last->shape.rot.y; + } + + // Spawns the four large stalactites to the left and right of Goht's ice block + for (; i < ARRAY_COUNT(this->stalactites); i++) { + last = (i < 8) ? this->stalactites[i - 6] : this->stalactites[i - 4]; + now = this->stalactites[i]; + + now->params = EN_HAKUROCK_TYPE_LARGE_STALACTITE; + now->world.pos.x = this->actor.world.pos.x; + now->world.pos.y = this->actor.world.pos.y; + now->world.pos.z = last->world.pos.z; + now->shape.rot.y = this->actor.shape.rot.y - 0x4000; + } +} + +void BossHakugin_RequestQuakeAndRumble(BossHakugin* this, PlayState* play, s16 speed, s32 verticalMag, s16 duration) { + s16 quakeIndex; + + if (verticalMag > 0) { + quakeIndex = Quake_Request(GET_ACTIVE_CAM(play), QUAKE_TYPE_3); + Quake_SetSpeed(quakeIndex, speed); + Quake_SetPerturbations(quakeIndex, verticalMag, 0, 0, 0); + Quake_SetDuration(quakeIndex, duration); + + Rumble_Request(this->actor.xyzDistToPlayerSq, 180, 20, 100); + } +} + +/** + * When Goht is frozen before the fight starts, this function has a 10% chance of spawning a single sparkle effect in a + * random position somewhere around the ice. + */ +void BossHakugin_SpawnIceSparkle(BossHakugin* this, PlayState* play) { + Vec3f pos; + + if (((this->actionFunc == BossHakugin_FrozenBeforeFight) || (this->actionFunc == BossHakugin_EntranceCutscene)) && + (Rand_ZeroOne() < 0.1f)) { + pos.x = + this->actor.world.pos.x + ((15.0f + (Rand_ZeroOne() * 15.0f)) * ((Rand_ZeroOne() < 0.5f) ? -1 : 1) * 4.0f); + pos.y = this->actor.world.pos.y + ((10.0f + (Rand_ZeroOne() * 90.0f)) * 2.7f); + pos.z = + this->actor.world.pos.z + ((15.0f + (Rand_ZeroOne() * 15.0f)) * ((Rand_ZeroOne() < 0.5f) ? -1 : 1) * 4.0f); + + EffectSsKirakira_SpawnDispersed(play, &pos, &gZeroVec3f, &gZeroVec3f, &sSparklePrimColor, &sSparkleEnvColor, + 2000, 5); + } +} + +/** + * Boosts the player's y-velocity and linear velocity whenever they jump off a ramp while Goron rolling with spikes. + * This makes the player's jump go much higher, and the increase to the linear velocity was seemingly done to make the + * player's speed in the air more closely match their speed on the ground. This boost is only applied once per jump. + */ +void BossHakugin_ApplyGoronSpikeBoost(BossHakugin* this, Player* player) { + if (!this->hasAppliedGoronSpikeBoost && (player->stateFlags3 & PLAYER_STATE3_80000) && + !(player->actor.bgCheckFlags & BGCHECKFLAG_GROUND) && (player->actor.velocity.y > 5.0f)) { + player->actor.velocity.y *= 1.3f; + player->linearVelocity *= 1.3f; + this->hasAppliedGoronSpikeBoost = true; + } else if (player->actor.bgCheckFlags & BGCHECKFLAG_GROUND) { + this->hasAppliedGoronSpikeBoost = false; + } +} + +/** + * Updates the position and radius of a single dynamic light. Since there's only one light, all of Goht's various + * effects have to share it, so the following list is used to determine priority; if an effect higher up the list is + * using the light, effects lower down the list will not be able to use it. In practice, it is uncommon for effects to + * overlap such that this priority list actually matters: + * - If an electric ball is active, the light will be attached to it. Since the electric ball is made up of multiple + * light orbs of different opacities, the light will be attached to the most-opaque light orb. + * - If Goht is charging up a lightning or electric ball attack, then the light will be attached to the charging effect + * in front of Goht's head. + * - If Goht is shooting lightning, then the light will be attached to the last lightning segment in the list that has + * an alpha of 255. This results in the light following the lightning down from Goht's head. + */ +void BossHakugin_UpdateLight(BossHakugin* this) { + Vec3f* lightPos = &this->actor.world.pos; + s32 i; + s16 radius = 0; + + for (i = 0; i < GOHT_LIGHTNING_SEGMENT_COUNT; i++) { + if (this->lightningSegments[i].alpha == 255) { + lightPos = &this->lightningSegments[i].pos; + radius = 5000; + break; + } + } + + if (i == GOHT_LIGHTNING_SEGMENT_COUNT) { + if (this->chargingLightOrbScale > 0.0f) { + lightPos = &this->chargingLightningPos; + radius = (this->chargingLightOrbScale / 30.0f) * 5000.0f; + } else if ((this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FLY_TOWARDS_PLAYER) || + (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FLY_FORWARD)) { + lightPos = &this->electricBallPos[0]; + radius = 5000; + } else if (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FADE_OUT) { + lightPos = &this->electricBallPos[this->electricBallCount]; + radius = (ARRAY_COUNT(this->electricBallPos) - this->electricBallCount) * 500.0f; + } + } + + Lights_PointNoGlowSetInfo(&this->lightInfo, lightPos->x, lightPos->y, lightPos->z, sLightColor.r, sLightColor.g, + sLightColor.b, radius); +} + +/** + * Spawns the blue warp at one of four predetermined locations based on Goht's x and z-coordinates, then spawns the + * heart container offset from the blue warp. + */ +void BossHakugin_SpawnBlueWarpAndHeartContainer(BossHakugin* this, PlayState* play) { + s16 atan = Math_Atan2S_XY(this->actor.world.pos.z, this->actor.world.pos.x); + Vec3f warpPos; + Vec3f heartPos; + f32 offset; + f32 sin; + f32 cos; + + if (atan > 0x4000) { + warpPos.x = 1400.0f; + warpPos.y = 0.0f; + warpPos.z = -1400.0f; + } else if (atan < -0x4000) { + warpPos.x = -1400.0f; + warpPos.y = 0.0f; + warpPos.z = -1400.0f; + } else if (atan > 0) { + warpPos.x = 1400.0f; + warpPos.y = -320.0f; + warpPos.z = 1400.0f; + } else { + warpPos.x = -1400.0f; + warpPos.y = -160.0f; + warpPos.z = 1400.0f; + } + + offset = this->direction * 300.0f; + sin = Math_SinS(this->targetRotY); + cos = Math_CosS(this->targetRotY); + heartPos.x = ((100.0f * sin) + warpPos.x) + (offset * cos); + heartPos.z = ((100.0f * cos) + warpPos.z) - (offset * sin); + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, warpPos.x, warpPos.y, warpPos.z, 0, 0, 0, + ENDOORWARP1_FF_1); + Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, heartPos.x, warpPos.y, heartPos.z, 0, 0, 0, + BHEART_PARAM_NORMAL); +} + +/** + * This function updates Goht's base y-rotation to make sure it turns to follow the curves in its arena. It also moves + * Goht away from the sides of the room if it strays too close to them. + * + * Goht's "base" rotation is one component of its final rotation, and it is specifically the component that perfectly + * follows the walls of the room; while running along the arena's straightaways, the base rotation is exactly parallel + * to the two side walls. This function uses the angles of the normals of the left and right wall polygons to determine + * how much Goht should turn to keep following the walls. + */ +void BossHakugin_UpdateBaseRot(BossHakugin* this, PlayState* play) { + CollisionPoly* rightWallPoly = NULL; + CollisionPoly* leftWallPoly = NULL; + s32 bgId; + s16 rightWallYawDiff; + s16 leftWallYawDiff; + s16 averagedYawDiff; + Vec3f rightWallPos; + Vec3f leftWallPos; + Vec3f posA; + Vec3f posB; + f32 offsetFromWall; + + // The initial point of our line is slightly above and slightly in front of Goht. The point is slightly above Goht's + // position because, near the floor of the arena, the walls are sloped, so elevating the check ensures it looks at + // the flat upper walls instead. The point is placed a variable distance in front of Goht depending on its speed so + // that it will turn sooner when it's running faster. + posA.x = this->actor.world.pos.x + (Math_SinS(this->baseRotY) * (5.0f * this->actor.speed)); + posA.y = this->actor.world.pos.y + 450.0f; + posA.z = this->actor.world.pos.z + (Math_CosS(this->baseRotY) * (5.0f * this->actor.speed)); + + posB.x = posA.x - Math_CosS(this->baseRotY) * 1000.0f; + posB.y = posA.y; + posB.z = posA.z + (Math_SinS(this->baseRotY) * 1000.0f); + + if (BgCheck_EntityLineTest1(&play->colCtx, &posA, &posB, &rightWallPos, &rightWallPoly, true, true, false, true, + &bgId)) { + rightWallYawDiff = + Math_Atan2S_XY(COLPOLY_GET_NORMAL(rightWallPoly->normal.z), COLPOLY_GET_NORMAL(rightWallPoly->normal.x)) - + this->baseRotY; + this->distToRightWall = Math_Vec3f_DistXZ(&posA, &rightWallPos); + } else { + // There is no wall close enough to Goht's right. In practice, this will never happen inside its boss arena. + rightWallYawDiff = 0; + this->distToRightWall = 30000.0f; + } + + posB.x = (2.0f * posA.x) - posB.x; + posB.z = (2.0f * posA.z) - posB.z; + + if (BgCheck_EntityLineTest1(&play->colCtx, &posA, &posB, &leftWallPos, &leftWallPoly, true, true, false, true, + &bgId)) { + leftWallYawDiff = + Math_Atan2S_XY(COLPOLY_GET_NORMAL(leftWallPoly->normal.z), COLPOLY_GET_NORMAL(leftWallPoly->normal.x)) - + this->baseRotY; + this->distToLeftWall = Math_Vec3f_DistXZ(&posA, &leftWallPos); + } else { + // There is no wall close enough to Goht's left. In practice, this will never happen inside its boss arena. + leftWallYawDiff = 0; + this->distToLeftWall = 30000.0f; + } + + if (this->direction == GOHT_DIRECTION_CLOCKWISE) { + this->distToLeftWall -= 50.0f; + } else { + this->distToRightWall -= 50.0f; + } + + if (this->distToRightWall <= (89100.0f * 0.001f)) { + offsetFromWall = (this->direction == GOHT_DIRECTION_CLOCKWISE) ? (89100.0f * 0.001f) : (139100.0f * 0.001f); + this->actor.world.pos.x = (rightWallPos.x + (offsetFromWall * Math_CosS(this->baseRotY))) - + (Math_SinS(this->baseRotY) * (5.0f * this->actor.speed)); + this->actor.world.pos.z = (rightWallPos.z - (offsetFromWall * Math_SinS(this->baseRotY))) - + (Math_CosS(this->baseRotY) * (5.0f * this->actor.speed)); + } else if (this->distToLeftWall <= (89100.0f * 0.001f)) { + offsetFromWall = (this->direction == GOHT_DIRECTION_CLOCKWISE) ? (139100.0f * 0.001f) : (89100.0f * 0.001f); + this->actor.world.pos.x = (leftWallPos.x - (offsetFromWall * Math_CosS(this->baseRotY))) - + (Math_SinS(this->baseRotY) * (5.0f * this->actor.speed)); + this->actor.world.pos.z = (leftWallPos.z + (offsetFromWall * Math_SinS(this->baseRotY))) - + (Math_CosS(this->baseRotY) * (5.0f * this->actor.speed)); + } + + if ((this->distToLeftWall < 30000.0f) && (this->distToRightWall < 30000.0f)) { + // If Goht is running along a straightaway (in other words, if the walls to its side are parallel), then the + // angles of the walls' normal vectors have the same absolute value; one is positive while the other is + // negative. As a result, the differences between these two angles and Goht's base rotation will cancel out. If + // Goht approaches a turn, however, this will not be the case, so the average difference is used to keep Goht + // parallel to the walls at this point in the turn. + averagedYawDiff = (s16)(rightWallYawDiff + leftWallYawDiff) >> 1; + if (((this->direction == GOHT_DIRECTION_CLOCKWISE) && (averagedYawDiff < 0)) || + ((this->direction == GOHT_DIRECTION_COUNTERCLOCKWISE) && (averagedYawDiff > 0))) { + this->baseRotY += averagedYawDiff; + } + } +} + +void BossHakugin_SetLightningSegmentColliderVertices(GohtLightningSegment* lightningSegment) { + s32 i; + Vec3f vertices[3]; + + Matrix_SetTranslateRotateYXZ(lightningSegment->pos.x, lightningSegment->pos.y, lightningSegment->pos.z, + &lightningSegment->rot); + Matrix_Scale(1.0f, 1.0f, 1.0f, MTXMODE_APPLY); + + for (i = 0; i < ARRAY_COUNT(vertices); i++) { + Matrix_MultVec3f(&sTrisElementsInit[0].dim.vtx[i], &vertices[i]); + } + + Collider_SetTrisVertices(&lightningSegment->collider, 0, &vertices[0], &vertices[1], &vertices[2]); +} + +/** + * Initializes the lightning segments that Goht shoots at the player. Each segment is initially rotated to point towards + * the player, but a random offset is added to the rotation so that the lightning doesn't travel in a perfectly straight + * line. The end result is a chain of lightning segments that generally travels towards the player, but does so in an + * unpredictable way. + */ +void BossHakugin_AddLightningSegments(BossHakugin* this, Vec3f* startPos, PlayState* play) { + s32 i; + Player* player = GET_PLAYER(play); + CollisionPoly* poly; + Vec3f rootPos; + Vec3f targetPos; + Vec3f nextRootPos; + Vec3f resultPos; + Vec3f transformedTargetPos; + Vec3f transformedRootPos; + GohtLightningSegment* lightningSegment; + s32 bgId; + + Math_Vec3f_Copy(&rootPos, startPos); + targetPos.x = player->actor.world.pos.x - (Math_SinS(this->actor.shape.rot.y) * 50.0f); + targetPos.y = player->actor.world.pos.y + 40.0f; + targetPos.z = player->actor.world.pos.z - (Math_CosS(this->actor.shape.rot.y) * 50.0f); + Actor_OffsetOfPointInActorCoords(&this->actor, &transformedTargetPos, &targetPos); + Audio_PlaySfx_AtPos(&this->sfxPos, NA_SE_EN_COMMON_THUNDER_THR); + + for (i = 0; i < GOHT_LIGHTNING_SEGMENT_COUNT; i++) { + lightningSegment = &this->lightningSegments[i]; + Actor_OffsetOfPointInActorCoords(&this->actor, &transformedRootPos, &rootPos); + + if (transformedTargetPos.z < transformedRootPos.z) { + lightningSegment->rot.y = this->actor.shape.rot.y + ((s32)Rand_Next() >> 0x13); + } else { + lightningSegment->rot.y = Math_Vec3f_Yaw(&rootPos, &targetPos) + ((s32)Rand_Next() >> 0x13); + } + + lightningSegment->rot.x = Math_Vec3f_Pitch(&rootPos, &targetPos) + ((s32)Rand_Next() >> 0x13); + lightningSegment->rot.z = 0; + lightningSegment->pos.x = + rootPos.x + (80.0f * Math_CosS(lightningSegment->rot.x) * Math_SinS(lightningSegment->rot.y)); + lightningSegment->pos.y = rootPos.y - (80.0f * Math_SinS(lightningSegment->rot.x)); + lightningSegment->pos.z = + rootPos.z + (80.0f * Math_CosS(lightningSegment->rot.x) * Math_CosS(lightningSegment->rot.y)); + nextRootPos.x = (2.0f * lightningSegment->pos.x) - rootPos.x; + nextRootPos.y = (2.0f * lightningSegment->pos.y) - rootPos.y; + nextRootPos.z = (2.0f * lightningSegment->pos.z) - rootPos.z; + + if (BgCheck_EntityLineTest1(&play->colCtx, &rootPos, &nextRootPos, &resultPos, &poly, false, true, false, true, + &bgId)) { + lightningSegment->rot.x -= 0x2000; + lightningSegment->pos.x = + rootPos.x + (80.0f * Math_CosS(lightningSegment->rot.x) * Math_SinS(lightningSegment->rot.y)); + lightningSegment->pos.y = rootPos.y - (80.0f * Math_SinS(lightningSegment->rot.x)); + lightningSegment->pos.z = + rootPos.z + (80.0f * Math_CosS(lightningSegment->rot.x) * Math_CosS(lightningSegment->rot.y)); + rootPos.x = (2.0f * lightningSegment->pos.x) - rootPos.x; + rootPos.y = (2.0f * lightningSegment->pos.y) - rootPos.y; + rootPos.z = (2.0f * lightningSegment->pos.z) - rootPos.z; + } else { + Math_Vec3f_Copy(&rootPos, &nextRootPos); + } + + lightningSegment->alpha = 255 + 20 * (i + 1); + BossHakugin_SetLightningSegmentColliderVertices(lightningSegment); + lightningSegment->rot.z = (s32)Rand_Next() >> 0x10; + } +} + +/** + * Spawns a small rock that flies backwards from the supplied position. This is called repeatedly while Goht is running + * to make it look like Goht's feet are kicking up rocks. + */ +void BossHakugin_SpawnGravel(BossHakugin* this, Vec3f* pos) { + s32 i; + GohtRockEffect* rockEffect; + + for (i = 0; i < GOHT_ROCK_EFFECT_COUNT; i++) { + rockEffect = &this->rockEffects[i]; + if (rockEffect->timer < 0) { + VecGeo velocityGeo; + + Math_Vec3f_Copy(&rockEffect->pos, pos); + velocityGeo.pitch = Rand_S16Offset(0x1000, 0x3000); + velocityGeo.yaw = this->actor.shape.rot.y + ((s32)Rand_Next() >> 0x12) + 0x8000; + velocityGeo.r = Rand_ZeroFloat(5.0f) + 7.0f; + rockEffect->velocity.x = velocityGeo.r * Math_CosS(velocityGeo.pitch) * Math_SinS(velocityGeo.yaw); + rockEffect->velocity.y = velocityGeo.r * Math_SinS(velocityGeo.pitch); + rockEffect->velocity.z = velocityGeo.r * Math_CosS(velocityGeo.pitch) * Math_CosS(velocityGeo.yaw); + rockEffect->pos.x = pos->x + (Rand_ZeroFloat(3.0f) * rockEffect->velocity.x); + rockEffect->pos.y = pos->y + (Rand_ZeroFloat(3.0f) * rockEffect->velocity.y); + rockEffect->pos.z = pos->z + (Rand_ZeroFloat(3.0f) * rockEffect->velocity.z); + rockEffect->scale = (Rand_ZeroFloat(6.0f) + 15.0f) * 0.0001f; + rockEffect->timer = 40; + rockEffect->type = GOHT_ROCK_EFFECT_TYPE_BOULDER; + break; + } + } +} + +/** + * This function can spawn a large boulder that flies into the air behind Goht and can damage the player. The boulder + * can only be spawned if the player isn't too far behind Goht and if Goht's speed is 10.0f or higher. Assuming those + * conditions have been met, the boulder still only has a 40% chance of spawning. + * + * This function will not spawn a boulder if the `preventBoulderSpawnCount` variable on Goht's instance is non-zero. + * Instead, calling this function will decrement the variable; when a boulder is spawned, this variable is set to 4 to + * prevent Goht from spawning a lot of boulders in a short period of time. + */ +void BossHakugin_SpawnBoulder(BossHakugin* this, Vec3f* pos) { + Actor* boulder; + s32 i; + + if (((this->transformedPlayerPos.z < -200.0f) && (this->actor.speed > 10.0f) && + (this->preventBoulderSpawnCount == 0) && (Rand_ZeroOne() < 0.4f))) { + this->preventBoulderSpawnCount = 4; + } else { + if (this->preventBoulderSpawnCount > 0) { + this->preventBoulderSpawnCount--; + } + return; + } + + for (i = 0; i < ARRAY_COUNT(this->boulders); i++) { + boulder = this->boulders[i]; + + if (EN_HAKUROCK_GET_TYPE(boulder) == EN_HAKUROCK_TYPE_NONE) { + Math_Vec3f_Copy(&boulder->world.pos, pos); + boulder->params = EN_HAKUROCK_TYPE_BOULDER; + break; + } + } +} + +/** + * This function can spawn a stalactite that falls to the floor and can slow the player to a stop if they Goron Roll + * into it. The stalactite can only be spawned if Goht's health is below 20. Assuming Goht's health is low enough, the + * stalactite still only has a 35% chance of spawning. + * + * This function will not spawn a stalactite if the `preventStalactiteSpawnCount` variable on Goht's instance is + * non-zero. Instead, calling this function will decrement the variable; when a stalactite is spawned, this variable is + * set to 4 to prevent Goht from spawning a lot of stalactites in a short period of time. + */ +void BossHakugin_SpawnStalactite(BossHakugin* this) { + Actor* stalactite; + s32 i; + + if ((this->preventStalactiteSpawnCount == 0) && (this->actor.colChkInfo.health < 20) && (Rand_ZeroOne() < 0.35f)) { + this->preventStalactiteSpawnCount = 4; + } else { + if (this->preventStalactiteSpawnCount > 0) { + this->preventStalactiteSpawnCount--; + } + return; + } + + for (i = 0; i < ARRAY_COUNT(this->stalactites); i++) { + stalactite = this->stalactites[i]; + + if (EN_HAKUROCK_GET_TYPE(stalactite) == EN_HAKUROCK_TYPE_NONE) { + stalactite->params = EN_HAKUROCK_TYPE_FALLING_STALACTITE; + return; + } + } +} + +/** + * This function can spawn a bomb that flies into the air behind Goht. The bomb can only be spawned if Goht's health is + * below 10 and if Goht's speed is 10.0f or higher. Assuming those conditions have been met, the bomb still only has a + * 35% chance of spawning. + * + * This function will not spawn a bomb if the `preventBombSpawnCount` variable on Goht's instance is non-zero. Instead, + * calling this function will decrement the variable; when a bomb is spawned, this variable is set to 4 to prevent Goht + * from spawning a lot of bombs in a short period of time. + */ +void BossHakugin_SpawnBomb(BossHakugin* this, PlayState* play) { + EnBom* bomb; + s16 yawDiff; + + if ((this->actor.speed > 10.0f) && ((s32)this->actor.colChkInfo.health < 10) && + (this->preventBombSpawnCount == 0) && (Rand_ZeroOne() < 0.35f)) { + this->preventBombSpawnCount = 4; + } else { + if (this->preventBombSpawnCount > 0) { + this->preventBombSpawnCount--; + } + return; + } + + bomb = (EnBom*)Actor_Spawn(&play->actorCtx, play, ACTOR_EN_BOM, + this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_THORAX].dim.worldSphere.center.x - + (100.0f * Math_SinS(this->actor.shape.rot.y)), + this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_THORAX].dim.worldSphere.center.y + + 100.0f, + this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_THORAX].dim.worldSphere.center.z - + (Math_CosS(this->actor.shape.rot.y) * 100.0f), + BOMB_EXPLOSIVE_TYPE_BOMB, 0, 0, BOMB_TYPE_BODY); + + if (bomb != NULL) { + yawDiff = (this->actor.yawTowardsPlayer - this->actor.shape.rot.y) - 0x8000; + if (yawDiff > 0x2000) { + bomb->actor.world.rot.y = this->actor.shape.rot.y + 0xA000; + } else if (yawDiff < -0x2000) { + bomb->actor.world.rot.y = this->actor.shape.rot.y + 0x6000; + } else { + bomb->actor.world.rot.y = this->actor.yawTowardsPlayer; + } + + bomb->timer = (Rand_Next() >> 0x1C) + 17; + bomb->actor.velocity.y = 2.0f; + bomb->actor.speed = this->actor.xzDistToPlayer * 0.01f; + bomb->actor.speed = CLAMP(bomb->actor.speed, 6.0f, 12.0f); + + Actor_SetScale(&bomb->actor, 0.02f); + } +} + +/** + * This function is responsible for spawning the smoke effects that billow from various body parts as Goht takes damage. + * It also spawns the related electrical effect from the same body parts, which is handled by `EffectSsFhgFlash`. These + * effects only begin to spawn when Goht has 15 or lower health (it starts with 30), and effects will appear on + * different body parts for every 3 health that Goht loses after that. + */ +void BossHakugin_AddMalfunctionEffects(BossHakugin* this, PlayState* play) { + GohtMalfunctionEffect* malfunctionEffect; + Vec3f* currentPos; + Vec3f* prevPos; + Vec3f diff; + s32 effectIndex; + s32 effectCount; + s32 type; + s32 i; + s32 pad; + + // The `effectIndex` variable is used here to represent the previous frame's effect index. The effect index is + // decreased by 3 every frame, so we add 3 here to counteract that. + effectIndex = this->malfunctionEffectIndex + 3; + if (effectIndex >= GOHT_MALFUNCTION_EFFECTS_PER_TYPE) { + effectIndex = 0; + } + + for (type = 0; type < GOHT_MALFUNCTION_NUM_TYPES; type++) { + if (((15 - (3 * type)) < this->actor.colChkInfo.health) || + !(GOHT_LIMB_DRAW_FLAG(sMalfunctionTypeToLimbIndex[type]) & this->limbDrawFlags)) { + break; + } + + prevPos = &this->malfunctionEffects[type][effectIndex].pos; + currentPos = &this->malfunctionEffects[type][this->malfunctionEffectIndex].pos; + Math_Vec3f_Diff(prevPos, currentPos, &diff); + + // This is done to counteract the fact that we add 3.5f to the malfunction effect's y-coordinate in + // `BossHakugin_UpdateMalfunctionEffects`, which is called once before the effect is ever rendered. If we didn't + // do this, the effect would appear to spawn a little "too high", though it would be very hard to notice. + diff.y -= 3.5f; + + // Depending on the difference between where the effect is on this frame compared to where it was on the + // previous frame, we can spawn 1 to 3 effects in the loop below. This is done so that, if Goht is running + // around, the smoke will "trail" behind him. + effectCount = Math3D_Vec3fMagnitude(&diff) / 10.0f; + if (effectCount > 1) { + effectCount = CLAMP_MAX(effectCount, 3); + Math_Vec3f_Scale(&diff, 1.0f / effectCount); + } else { + effectCount = 1; + } + + for (i = 0; i < effectCount; i++) { + malfunctionEffect = &this->malfunctionEffects[type][this->malfunctionEffectIndex + i]; + + malfunctionEffect->pos.x = Rand_CenteredFloat(20.0f) + (currentPos->x + (diff.x * i)); + malfunctionEffect->pos.y = Rand_CenteredFloat(20.0f) + (currentPos->y + (diff.y * i)); + malfunctionEffect->pos.z = Rand_CenteredFloat(20.0f) + (currentPos->z + (diff.z * i)); + malfunctionEffect->scaleXY = 0.01f; + malfunctionEffect->alpha = 150; + malfunctionEffect->timer = 5 - i; + } + + if ((play->gameplayFrames % 2) != 0) { + EffectSsFhgFlash_SpawnShock(play, &this->actor, + &this->malfunctionEffects[type][this->malfunctionEffectIndex].pos, 250, + FHGFLASH_SHOCK_GOHT_BACK_LEFT_THIGH + type); + } + } + + // The `effectIndex` variable is used here to represent the value `malfunctionEffectIndex` should have on the next + // frame. Because of the loop above, up to three effects can be initialized for any given `malfunctionEffectIndex`, + // so we subtract 3 from the current index to make room for those three potential effects. + effectIndex = this->malfunctionEffectIndex - 3; + if (effectIndex < 0) { + this->malfunctionEffectIndex = GOHT_MALFUNCTION_EFFECTS_PER_TYPE - 3; + } else { + this->malfunctionEffectIndex = effectIndex; + } +} + +/** + * This function is responsible for multiple things which are all related to Goht charging up its electric ball or + * lightning attacks. Actions that want Goht to charge up an attack should call this function repeatedly on every frame. + * - If the charge up timer is 5, this function will make Goht slowly raise its head and increase the scale of its + * charging lightning. When the head reaches its target rotation, the charge up timer will be decremented to signal + * that we should move onto the next phase. + * - If the charge up timer is above 0 but below 5, this function will do nothing besides decrement the charge up timer. + * It will also do that if the charge up timer is above 5, though this never happens in the final game. + * - If the charge up timer is 0, this function will make Goht lower its head quickly. Once the head reaches its target + * rotation, this function will return true. + * + * The return value of this function is used to indicate whether Goht should "fire off" its attack or not. When it + * returns true, Goht should unleash its charged electric ball or lightning attack. When it returns false, this + * indicates that Goht should keep charging its attack. + */ +s32 BossHakugin_ChargeUpAttack(BossHakugin* this) { + s16 rand = (Rand_Next() >> 0x12) + 0x4000; + + this->lightOrbRotZ += rand; + if (this->chargeUpTimer == 5) { + if (!Math_SmoothStepToS(&this->frontHalfRotZ, -0x900, 4, 0x200, 0x80)) { + this->chargeUpTimer--; + } + + this->chargingLightOrbScale = (this->frontHalfRotZ + 0x480) * (-30.0f / 0x480); + this->chargingLightOrbScale = CLAMP_MIN(this->chargingLightOrbScale, 0.001f); + + this->chargingLightningScale = (this->frontHalfRotZ - 0x700) * (-0.62f / 0xB80); + this->chargingLightningScale = CLAMP_MAX(this->chargingLightningScale, 0.62f); + + if (this->chargingLightningScale > 0.0f) { + this->chargingLightningTranslateZ = -100.0f - (((0.62f / this->chargingLightningScale) - 1.0f) * 200.0f); + } else { + this->chargingLightningTranslateZ = -200.0f; + } + } else if (this->chargeUpTimer > 0) { + this->chargeUpTimer--; + } else if (Math_ScaledStepToS(&this->frontHalfRotZ, 0x700, 0x380)) { + this->chargingLightningScale = (0x700 - this->frontHalfRotZ) / 4096.0f * 0.62f; + return true; + } + + return false; +} + +/** + * Handles functionality common to all of Goht's running actions, including: + * - Playing Goht's footstep sounds + * - Requesting a quake and rumble when the player is close to Goht + * - Spawning boulders, stalactites, and bombs under certain conditions to damage or impede the player + * - Spwaning dust and gravel effects at Goht's feet whenever they hit the ground + */ +void BossHakugin_RunUpdateCommon(BossHakugin* this, PlayState* play) { + Vec3f velocity; + Vec3f pos; + f32 quakeVerticalMag; + s32 i; + + if (Animation_OnFrame(&this->skelAnime, 0.0f)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_FOOTSTEP_OLD); + Math_Vec3f_Copy(&pos, &this->bodyPartsPos[sLimbToBodyParts[GOHT_LIMB_FRONT_LEFT_HOOF]]); + if (this->actor.xzDistToPlayer < 1500.0f) { + quakeVerticalMag = (1500.0f - this->actor.xzDistToPlayer) * (1.0f / 1500.0f); + BossHakugin_RequestQuakeAndRumble(this, play, 17000, 6.5f * quakeVerticalMag, 10); + } + + BossHakugin_SpawnStalactite(this); + } else if (Animation_OnFrame(&this->skelAnime, 2.0f)) { + Math_Vec3f_Copy(&pos, &this->bodyPartsPos[sLimbToBodyParts[GOHT_LIMB_BACK_LEFT_HOOF]]); + BossHakugin_SpawnBoulder(this, &pos); + } else if (Animation_OnFrame(&this->skelAnime, 3.0f)) { + Math_Vec3f_Copy(&pos, &this->bodyPartsPos[sLimbToBodyParts[GOHT_LIMB_FRONT_RIGHT_HOOF]]); + BossHakugin_SpawnBomb(this, play); + } else if (Animation_OnFrame(&this->skelAnime, 11.0f)) { + Math_Vec3f_Copy(&pos, &this->bodyPartsPos[sLimbToBodyParts[GOHT_LIMB_BACK_RIGHT_HOOF]]); + BossHakugin_SpawnBoulder(this, &pos); + } else { + return; + } + + velocity.x = Math_SinS(this->actor.shape.rot.y) * -1.0f; + velocity.y = 1.0f; + velocity.z = Math_CosS(this->actor.shape.rot.y) * -1.0f; + pos.x += 40.0f * velocity.x; + pos.y += 5.0f; + pos.z += 40.0f * velocity.z; + func_800B12F0(play, &pos, &velocity, &gZeroVec3f, Rand_S16Offset(650, 100), Rand_S16Offset(20, 10), 30); + + for (i = 0; i < 4; i++) { + BossHakugin_SpawnGravel(this, &pos); + } +} + +/** + * Spawns steam effects that are used when Goht is thawed out in its intro cutscene. The steam will usually spawn in + * random locations around Goht's body, however if `steamFollowsMeltingIce` is true, then the steam will spawn with + * increasingly-lower y-coordinates as the ice block that surrounds Goht melts and shrinks downwards. + */ +void BossHakugin_SpawnSteam(BossHakugin* this, PlayState* play, s32 steamFollowsMeltingIce) { + Vec3f velocity; + Vec3f pos; + + if (steamFollowsMeltingIce) { + velocity.y = (this->timer - 30) * (1 / 70.0f); + } else { + velocity.y = 0.1f; + } + + velocity.x = Rand_CenteredFloat(2.0f) * velocity.y; + velocity.z = Rand_CenteredFloat(2.0f) * velocity.y; + + pos.x = ((velocity.x >= 0.0f ? 1.0f : -1.0f) * (Rand_ZeroFloat(20.0f) + 5.0f) * 4.0f) + this->iceCollider.dim.pos.x; + pos.z = ((velocity.z >= 0.0f ? 1.0f : -1.0f) * (Rand_ZeroFloat(20.0f) + 5.0f) * 4.0f) + this->iceCollider.dim.pos.z; + + if (steamFollowsMeltingIce) { + pos.y = this->iceCollider.dim.pos.y + (this->iceCollider.dim.height * velocity.y); + } else { + pos.y = (Rand_ZeroFloat(this->iceCollider.dim.height) * 0.8f) + this->iceCollider.dim.pos.y; + } + + velocity.y += 0.8f; + EffectSsIceSmoke_Spawn(play, &pos, &velocity, &gZeroVec3f, 600); +} + +/** + * Returns true if Goht should stop running and either wait for the player to get in view (to shoot lightning at them) + * or wait 150 frames (after which, Goht will turn around and charge them). + */ +s32 BossHakugin_ShouldWait(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + s16 yawDiff; + s32 absYawDiff; + f32 posZ; + f32 posX; + + if (this->transformedPlayerPos.z > 0.0f) { + return false; + } + + yawDiff = this->baseRotY - Camera_GetCamDirYaw(GET_ACTIVE_CAM(play)); + absYawDiff = ABS_ALT(yawDiff); + + if (absYawDiff < 0x4000) { + if (absYawDiff < 0x1800) { + this->lightningHitSomething = false; + } + return false; + } + + // If Goht hit the player with lightning the last time it waited, then the only way for `lightningHitSomething` to + // be false (in other words, the only way for us continue in this function) is because it was set to false by the + // code above in a previous call to this function; this only happens if the difference between Goht's base yaw and + // the active cam's yaw is small enough. + if (this->lightningHitSomething == true) { + return false; + } + + posX = this->actor.world.pos.x * this->direction; + posZ = this->actor.world.pos.z * this->direction; + + /** + * Goht's arena is a square with rounded corners. If the player is standing in one of the "sides" of the square, and + * Goht is standing near one of the opposite corners (which specific corner depends on whether Goht is running + * clockwise or counterclockwise), then Goht will wait. The diagram below illustrates this; if the player is + * standing in the box labeled P, and Goht is standing in the box G1 (if Goht is running counterclockwise) or G2 (if + * Goht is running clockwise), then Goht will wait: + * _____________________ + * / | | \ + * / | P | \ + * | |___________| | + * | | | | + * |______| |______| + * | | | | + * | G1 | | G2 | + * | |‾‾‾‾‾‾‾‾‾‾‾| | + * \ | | / + * \____|___________|____/ + */ + if (((player->actor.world.pos.x > 1200.0f) && (player->actor.world.pos.z < 1200.0f) && + (player->actor.world.pos.z > -1200.0f) && (this->actor.world.pos.x < 0.0f) && (posZ > 1200.0f)) || + ((player->actor.world.pos.x < -1200.0f) && (player->actor.world.pos.z < 1200.0f) && + (player->actor.world.pos.z > -1200.0f) && (this->actor.world.pos.x > 0.0f) && (posZ < -1200.0f)) || + ((player->actor.world.pos.z > 1200.0f) && (player->actor.world.pos.x < 1200.0f) && + (player->actor.world.pos.x > -1200.0f) && (this->actor.world.pos.z < 0.0f) && (posX < -1200.0f)) || + ((player->actor.world.pos.z < -1200.0f) && (player->actor.world.pos.x < 1200.0f) && + (player->actor.world.pos.x > -1200.0f) && (this->actor.world.pos.z > 0.0f) && (posX > 1200.0f))) { + return true; + } + + return false; +} + +/** + * Spawns ice effects from Goht's body parts that fly off in random directions. Not to be confused with + * `BossHakugin_IntroCutsceneThaw`, which is exclusively used during the intro cutscene; this function is used during + * gameplay when Goht is hit by an Ice Arrow. + */ +void BossHakugin_Thaw(BossHakugin* this, PlayState* play) { + if (this->drawDmgEffType == ACTOR_DRAW_DMGEFF_FROZEN_NO_SFX) { + this->drawDmgEffType = ACTOR_DRAW_DMGEFF_FIRE; + this->drawDmgEffAlpha = 0.0f; + Actor_SpawnIceEffects(play, &this->actor, this->bodyPartsPos, GOHT_BODYPART_MAX, 3, 0.7f, 0.5f); + } +} + +/** + * Handles various things related to damage effects, like setting the effect alpha and scale and spawning light rays at + * the appropriate position when Goht is hit by Light Arrows or by a sword beam. + */ +void BossHakugin_UpdateDrawDmgEffect(BossHakugin* this, PlayState* play, s32 colliderIndex) { + if (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_FIRE) { + this->drawDmgEffType = ACTOR_DRAW_DMGEFF_FIRE; + this->drawDmgEffAlpha = 3.0f; + this->drawDmgEffScale = 2.5f; + } else if (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_LIGHT_ORB) { + this->drawDmgEffType = ACTOR_DRAW_DMGEFF_LIGHT_ORBS; + this->drawDmgEffAlpha = 3.0f; + this->drawDmgEffScale = 2.5f; + Actor_Spawn(&play->actorCtx, play, ACTOR_EN_CLEAR_TAG, + this->bodyCollider.elements[colliderIndex].info.bumper.hitPos.x, + this->bodyCollider.elements[colliderIndex].info.bumper.hitPos.y, + this->bodyCollider.elements[colliderIndex].info.bumper.hitPos.z, 0, 0, 0, + CLEAR_TAG_PARAMS(CLEAR_TAG_LARGE_LIGHT_RAYS)); + } else if (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_FREEZE) { + this->drawDmgEffType = ACTOR_DRAW_DMGEFF_FROZEN_NO_SFX; + this->drawDmgEffScale = 2.5f; + this->drawDmgEffFrozenSteamScale = 3.75f; + this->drawDmgEffAlpha = 1.0f; + } else if (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_BLUE_LIGHT_ORB) { + this->drawDmgEffType = ACTOR_DRAW_DMGEFF_BLUE_LIGHT_ORBS; + this->drawDmgEffScale = 2.5f; + this->drawDmgEffAlpha = 3.0f; + Actor_Spawn(&play->actorCtx, play, ACTOR_EN_CLEAR_TAG, + this->bodyCollider.elements[colliderIndex].info.bumper.hitPos.x, + this->bodyCollider.elements[colliderIndex].info.bumper.hitPos.y, + this->bodyCollider.elements[colliderIndex].info.bumper.hitPos.z, 0, 0, 3, + CLEAR_TAG_PARAMS(CLEAR_TAG_LARGE_LIGHT_RAYS)); + } +} + +/** + * The intention behind this function is to call it repeatedly to gradually move the subcam around and rotate it to its + * target rotation. The amount by which the position of the subcam's eye is updated per call depends on `posStep`, and + * the amount by which to rotate the subcam towards its target per call is determined by `angleStep`. + */ +void BossHakugin_UpdateSubCam(BossHakugin* this, PlayState* play, f32 posStep, s16 angleStep) { + Camera* subCam = Play_GetCamera(play, this->subCamId); + Vec3f diff; + Vec3f targetDiff; + + Math_Vec3f_Diff(&subCam->at, &subCam->eye, &diff); + Math_Vec3f_StepTo(&subCam->eye, &this->subCamEye, posStep); + + if (BossHakugin_Vec3fNormalize(&diff)) { + targetDiff.x = Math_CosS(this->subCamRot.x) * Math_SinS(this->subCamRot.y); + targetDiff.y = Math_SinS(this->subCamRot.x); + targetDiff.z = Math_CosS(this->subCamRot.x) * Math_CosS(this->subCamRot.y); + BossHakugin_StepVector(&diff, &targetDiff, BINANG_TO_RAD(angleStep)); + } + + subCam->at.x = subCam->eye.x + (50.0f * diff.x); + subCam->at.y = subCam->eye.y + (50.0f * diff.y); + subCam->at.z = subCam->eye.z + (50.0f * diff.z); + Play_SetCameraAtEye(play, this->subCamId, &subCam->at, &subCam->eye); +} + +void BossHakugin_SetupEntranceCutscene(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + Vec3f subCamAt; + + Animation_PlayOnce(&this->skelAnime, &gGohtThawAndBreakWallAnim); + this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; + + this->subCamId = CutsceneManager_GetCurrentSubCamId(this->actor.csId); + + subCamAt.x = player->actor.focus.pos.x; + subCamAt.y = player->actor.focus.pos.y; + subCamAt.z = player->actor.focus.pos.z; + + this->subCamEye.x = player->actor.focus.pos.x + (Math_SinS(player->actor.shape.rot.y) * 70.0f); + this->subCamEye.y = player->actor.focus.pos.y + 20.0f; + this->subCamEye.z = player->actor.focus.pos.z + (Math_CosS(player->actor.shape.rot.y) * 70.0f); + + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &this->subCamEye); + + this->subCamRot.x = Math_Atan2S_XY(70.0f, -20.0f); + this->subCamRot.y = player->actor.shape.rot.y + 0x8000; + Player_SetCsActionWithHaltedActors(play, &this->actor, PLAYER_CSACTION_21); + this->timer = 0; + this->actionFunc = BossHakugin_EntranceCutscene; +} + +/** + * Handles the cutscene that plays when the player first enters Goht's arena which shows it encased in ice. After 120 + * frames, the cutscene ends and control is returned to the player. + */ +void BossHakugin_EntranceCutscene(BossHakugin* this, PlayState* play) { + static Vec3f sSubCamAt = { 492.0f, 28.0f, -1478.0f }; + + this->timer++; + if (this->timer == 30) { + this->subCamEye.x = 417.0f; + this->subCamEye.y = 40.0f; + this->subCamEye.z = -1417.0f; + Play_SetCameraAtEye(play, this->subCamId, &sSubCamAt, &this->subCamEye); + func_800BE33C(&this->subCamEye, &sSubCamAt, &this->subCamRot, true); + } else if (this->timer == 50) { + this->subCamRot.x = 0x1500; + } else if (this->timer == 120) { + CutsceneManager_Stop(this->actor.csId); + SET_EVENTINF(EVENTINF_ENTR_CS_WATCHED_GOHT); + BossHakugin_SetupFrozenBeforeFight(this); + return; + } + + if (this->timer > 30) { + BossHakugin_UpdateSubCam(this, play, 5.0f, 0xC0); + } +} + +void BossHakugin_SetupFrozenBeforeFight(BossHakugin* this) { + this->timer = 0; + this->actionFunc = BossHakugin_FrozenBeforeFight; +} + +/** + * Waits around until the player strikes the ice block with a Fire Arrow, which starts Goht's intro cutscene. In this + * state, Goht's ice block can freeze the player if they touch it; if this happens, this function will ensure the player + * can't be frozen by the ice block again until 40 frames pass. + */ +void BossHakugin_FrozenBeforeFight(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + + if (!(player->stateFlags2 & PLAYER_STATE2_4000)) { + DECR(this->timer); + } + + if ((this->iceCollider.base.acFlags & AC_HIT) && + (this->iceCollider.info.acHitInfo->toucher.dmgFlags == DMG_FIRE_ARROW)) { + this->iceCollider.base.atFlags &= ~AT_HIT; + this->iceCollider.base.acFlags &= ~AC_HIT; + this->iceCollider.base.ocFlags1 &= ~OC1_HIT; + BossHakugin_SetupCutsceneStart(this); + return; + } + + if (this->iceCollider.base.atFlags & AT_HIT) { + this->timer = 40; + this->iceCollider.base.atFlags &= ~AT_HIT; + } + + if (this->timer == 0) { + CollisionCheck_SetAT(play, &play->colChkCtx, &this->iceCollider.base); + } + + CollisionCheck_SetAC(play, &play->colChkCtx, &this->iceCollider.base); + CollisionCheck_SetOC(play, &play->colChkCtx, &this->iceCollider.base); + this->disableBodyCollidersTimer = 20; +} + +void BossHakugin_SetupIntroCutsceneThaw(BossHakugin* this) { + this->timer = 100; + this->subCamId = CutsceneManager_GetCurrentSubCamId(this->actor.csId); + Play_EnableMotionBlur(140); + this->actionFunc = BossHakugin_IntroCutsceneThaw; +} + +/** + * Handles the first part of Goht's intro cutscene which shows close ups of various parts of Goht while the ice + * surrounding it melts and spawns steam. After 100 frames, this function switches to the action where Goht wakes up. + */ +void BossHakugin_IntroCutsceneThaw(BossHakugin* this, PlayState* play) { + Vec3f subCamEye; + Vec3f subCamAt; + s32 alpha; + + this->timer--; + if ((this->timer >= 30) && ((this->timer % 2) != 0)) { + BossHakugin_SpawnSteam(this, play, true); + } else if ((this->timer % 4) == 0) { + BossHakugin_SpawnSteam(this, play, false); + } + + if (this->timer == 99) { + subCamAt.x = 479.0f; + subCamAt.y = 181.0f; + subCamAt.z = -1637.0f; + subCamEye.x = 328.0f; + subCamEye.y = 164.0f; + subCamEye.z = -1693.0f; + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &subCamEye); + } else if (this->timer == 75) { + subCamAt.x = 693.0f; + subCamAt.y = 162.0f; + subCamAt.z = -1550.0f; + subCamEye.x = 638.0f; + subCamEye.y = 200.0f; + subCamEye.z = -1407.0f; + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &subCamEye); + } else if (this->timer == 50) { + subCamAt.x = 825.0f; + subCamAt.y = 123.0f; + subCamAt.z = -1637.0f; + subCamEye.x = 734.0f; + subCamEye.y = 145.0f; + subCamEye.z = -1762.0f; + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &subCamEye); + } else if (this->timer == 25) { + subCamAt.x = 711.0f; + subCamAt.y = 76.0f; + subCamAt.z = -1602.0f; + subCamEye.x = 634.0f; + subCamEye.y = 54.0f; + subCamEye.z = -1735.0f; + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &subCamEye); + } + + this->iceScaleY -= 0.027f; + alpha = (s32)(this->timer * 5.1f) + 50; + this->iceAlpha = CLAMP_MAX(alpha, 255); + + Actor_PlaySfx_Flagged(&this->actor, NA_SE_EV_ICE_MELT_LEVEL - SFX_FLAG); + + if (this->timer == 0) { + this->iceAlpha = 0; + BossHakugin_SetupIntroCutsceneWakeUp(this, play); + } +} + +void BossHakugin_SetupIntroCutsceneWakeUp(BossHakugin* this, PlayState* play) { + static Vec3f sSubCamAt = { 894.0f, 176.0f, -1600.0f }; + static Vec3f sSubCamEye = { 972.0f, 176.0f, -1600.0f }; + Player* player = GET_PLAYER(play); + + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_STEAM_DEMO_UP_OLD); + Play_SetCameraAtEye(play, this->subCamId, &sSubCamAt, &sSubCamEye); + this->subCamRot.x = -0x1F00; + this->subCamRot.y = -0x4000; + this->timer = 70; + this->subCamEye.x = 680.0f; + this->subCamEye.y = 370.0f; + this->subCamEye.z = -1600.0f; + player->actor.world.pos.x = 250.0f; + player->actor.world.pos.z = -1560.0f; + player->actor.shape.rot.y = this->actor.yawTowardsPlayer + 0x8000; + player->currentYaw = player->actor.world.rot.y = player->actor.shape.rot.y; + Player_SetCsActionWithHaltedActors(play, &this->actor, PLAYER_CSACTION_131); + this->actionFunc = BossHakugin_IntroCutsceneWakeUp; +} + +/** + * Handles the second part of Goht's intro cutscene where Goht wakes up from being frozen. For the first 70 frames of + * the cutscene, the camera travels up Goht's back until the player is in view in front of Goht. Afterwards, the camera + * focuses on Goht head-on as it looks around and destroys the wall of stalactites to its sides; this part of the + * cutscene is where the boss theme begins playing and Goht's title card appears. + */ +void BossHakugin_IntroCutsceneWakeUp(BossHakugin* this, PlayState* play) { + static Vec3f sSubCamAt = { 377.0f, 140.0f, -1600.0f }; + static Vec3f sSubCamEye = { 282.0f, 108.0f, -1600.0f }; + s32 i; + + this->timer--; + if (this->timer > 0) { + BossHakugin_UpdateSubCam(this, play, 7.0f, 0xC0); + } else if (this->timer == 0) { + Play_SetCameraAtEye(play, this->subCamId, &sSubCamAt, &sSubCamEye); + this->subCamRot.y = 0x4000; + this->subCamRot.x = Math_Atan2S_XY(95.0f, 32.0f); + this->subCamEye.x = 232.0f; + this->subCamEye.y = 43.0f; + this->subCamEye.z = -1600.0f; + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_STEAMS_DEMO_OLD); + } else { + BossHakugin_UpdateSubCam(this, play, 1.0f, 0xC0); + + if (Animation_OnFrame(&this->skelAnime, 20.0f)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_STEAM_DEMO_UP_OLD); + } + + if (Animation_OnFrame(&this->skelAnime, 45.0f)) { + SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 0, NA_BGM_BOSS | SEQ_FLAG_ASYNC); + } + + if (Animation_OnFrame(&this->skelAnime, 65.0f)) { + TitleCard_InitBossName(&play->state, &play->actorCtx.titleCtxt, Lib_SegmentedToVirtual(&gGohtTitleCardTex), + 160, 180, 128, 40); + } + + if (Animation_OnFrame(&this->skelAnime, 86.0f)) { + Audio_PlaySfx_AtPos(&this->sfxPos, NA_SE_EN_ICEB_CRY_OLD); + } + + if (Animation_OnFrame(&this->skelAnime, 90.0f)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_LAST1_DEMO_BREAK); + for (i = 6; i < ARRAY_COUNT(this->stalactites); i++) { + this->stalactites[i]->params = EN_HAKUROCK_TYPE_NONE; + } + } + + if (SkelAnime_Update(&this->skelAnime)) { + BossHakugin_SetupIntroCutsceneRun(this, play); + return; + } + } + + if (((this->timer + 12) >= 0) && ((this->timer % 4) == 0)) { + BossHakugin_SpawnSteam(this, play, false); + } +} + +void BossHakugin_SetupIntroCutsceneRun(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + Vec3f subCamAt; + + this->subCamEye.x = player->actor.world.pos.x - 150.0f; + this->subCamEye.y = player->actor.world.pos.y + 30.0f; + this->subCamEye.z = player->actor.world.pos.z - 30.0f; + subCamAt.x = this->actor.world.pos.x - 100.0f; + subCamAt.y = this->actor.world.pos.y + 100.0f; + subCamAt.z = this->actor.world.pos.z; + func_800BE33C(&this->subCamEye, &subCamAt, &this->subCamRot, true); + Animation_Change(&this->skelAnime, &gGohtRunAnim, 1.5f, 0.0f, 0.0f, ANIMMODE_LOOP, -3.0f); + this->timer = 0; + this->actor.speed = 5.0f; + Player_SetCsActionWithHaltedActors(play, &this->actor, PLAYER_CSACTION_END); + player->stateFlags1 |= PLAYER_STATE1_20; + play->actorCtx.isOverrideInputOn = true; + this->actionFunc = BossHakugin_IntroCutsceneRun; +} + +/** + * Handles the final part of Goht's intro cutscene where Goht runs forward and smashes through the wall of stalactites + * in front of it. This function manually overrides the control stick input to make the player run away from Goht, and + * it manually knocks the player back when Goht collides with them to make it appear as if Goht is running them over. + */ +void BossHakugin_IntroCutsceneRun(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + Camera* subCam = Play_GetCamera(play, this->subCamId); + Vec3f subCamAt; + f32 controlStickMagnitude; + s32 i; + + Math_ScaledStepToS(&this->frontHalfRotZ, 0x800, 0x80); + Math_StepToF(&this->actor.speed, 25.0f, 0.5f); + this->skelAnime.playSpeed = (this->actor.speed / 32.0f) + 0.5f; + BossHakugin_RunUpdateCommon(this, play); + + // Setting this to 10 prevents Goht from spawning boulders during the cutscene and shortly afterwards, since + // `BossHakugin_SpawnBoulder` is called fewer than 10 times during the cutscene. + this->preventBoulderSpawnCount = 10; + + SkelAnime_Update(&this->skelAnime); + this->timer++; + if (this->timer < 8) { + BossHakugin_UpdateSubCam(this, play, 25.0f, 0x120); + } else if (this->timer == 22) { + this->subCamEye.x = subCam->eye.x; + this->subCamEye.y = subCam->eye.y; + this->subCamEye.z = subCam->eye.z + 450.0f; + } else if (this->timer == 62) { + this->actor.flags |= ACTOR_FLAG_TARGETABLE; + CutsceneManager_Stop(this->actor.csId); + player->stateFlags1 &= ~PLAYER_STATE1_20; + SET_EVENTINF(EVENTINF_INTRO_CS_WATCHED_GOHT); + Play_DisableMotionBlur(); + BossHakugin_SetupRun(this); + return; + } else if (this->timer > 22) { + subCamAt.x = this->actor.world.pos.x; + subCamAt.y = this->actor.world.pos.y + 150.0f; + subCamAt.z = this->actor.world.pos.z; + Math_Vec3f_StepTo(&subCam->eye, &this->subCamEye, 35.0f); + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &subCam->eye); + + if (this->timer == 42) { + Actor_PlaySfx(&this->actor, NA_SE_EN_LAST1_DEMO_WALL); + for (i = 0; i < 6; i++) { + this->stalactites[i]->params = EN_HAKUROCK_TYPE_NONE; + } + } + } + + if (this->bodyCollider.base.atFlags & AT_HIT) { + func_800B8D50(play, &this->actor, 10.0f, 0, 6.0f, 0); + } else { + play->actorCtx.isOverrideInputOn = true; + } + + if (this->timer > 4) { + controlStickMagnitude = (this->timer - 4) * 5.0f; + controlStickMagnitude = CLAMP_MAX(controlStickMagnitude, 60.0f); + Actor_SetControlStickData(play, &play->actorCtx.overrideInput, controlStickMagnitude, -0x4000); + } +} + +void BossHakugin_SetupRun(BossHakugin* this) { + Animation_Change(&this->skelAnime, &gGohtRunAnim, 1.0f, 0.0f, 0.0f, ANIMMODE_LOOP_INTERP, -5.0f); + this->runOffsetRot = 0; + this->chargingLightOrbScale = 0.0f; + this->targetRotY = Rand_CenteredFloat(3072.0f); + this->timer = 40; + this->targetSpeed = 16.0f; + this->actionFunc = BossHakugin_Run; +} + +/** + * This function handles everything for when Goht is running in a circle around the boss arena, including: + * - Charging up and firing an electric ball when Goht is hit by Goron Spikes + * - Checking if Goht should wait around instead running and transitioning it to the waiting state if it should + * - Changing Goht's target speed and smooth-stepping its actual speed to match the target speed + * - Changing the play speed of Goht's running animation depending on its actual speed + * - Giving Goht's run a slightly-random drift so that it doesn't run perfectly straight + * - Checking if Goht should charge the player and transitioning it to the charging state if it should + */ +void BossHakugin_Run(BossHakugin* this, PlayState* play) { + s16 yawDiff; + + if (this->disableBodyCollidersTimer == 0) { + this->bodyCollider.base.atFlags |= AT_ON; + } + + if (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_CHARGE) { + if (BossHakugin_ChargeUpAttack(this)) { + Math_Vec3f_Copy(&this->electricBallPos[0], &this->chargingLightningPos); + this->electricBallSpeed = this->actor.speed + 100.0f; + this->electricBallRot.x = Math_CosS(0xA00) * Math_SinS(this->actor.shape.rot.y); + this->electricBallRot.y = Math_SinS(0xA00); + this->electricBallRot.z = Math_CosS(0xA00) * Math_CosS(this->actor.shape.rot.y); + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_FLY_FORWARD; + this->chargingLightOrbScale = 0.0f; + Audio_PlaySfx_AtPos(&this->sfxPos, NA_SE_EN_COMMON_E_BALL_THR); + this->electricBallCollider.base.atFlags |= AT_ON; + } + } else { + Math_ScaledStepToS(&this->frontHalfRotZ, 0, 0x100); + } + + if (BossHakugin_ShouldWait(this, play)) { + BossHakugin_SetupWait(this, play); + } else { + // The `damagedSpeedUpCounter` increases by 35 every time the player hits Goht (when it's not downed) and + // decreases by 1 every frame. Thus, if the player repeatedly hits Goht in a short amount of time, this will + // exceed 55; in this circumstance, Goht will run twice as fast as it normally does to get away from the player. + if (this->damagedSpeedUpCounter > 55) { + this->damagedSpeedUpCounter = 0; + this->timer = 40; + this->targetSpeed = 27.0f; + } + + if ((this->timer == 40) && + (Math_SmoothStepToF(&this->actor.speed, this->targetSpeed, 0.15f, 1.0f, 0.1f) < 0.05f)) { + this->timer--; + } else if (this->timer != 40) { + this->timer--; + if (this->timer == 0) { + if (this->actor.xzDistToPlayer > 1500.0f) { + this->targetSpeed = 12.5f; + } else if (this->transformedPlayerPos.z > 0.0f) { + this->targetSpeed = 21.0f; + } else { + this->targetSpeed = 14.5f; + } + + this->targetSpeed += (30 - this->actor.colChkInfo.health) * (1.0f / 30.0f); + this->timer = 40; + } + } + + this->skelAnime.playSpeed = (this->actor.speed / 32.0f) + 0.5f; + SkelAnime_Update(&this->skelAnime); + BossHakugin_RunUpdateCommon(this, play); + + if ((this->distToRightWall < (this->distToLeftWall * 0.5f)) || (this->distToRightWall < (89100.0f * 0.001f))) { + this->targetRotY = (Rand_ZeroFloat(0.4f) + 0.6f) * 1536.0f; + } else if ((this->distToLeftWall < (this->distToRightWall * 0.5f)) || + (this->distToLeftWall < (89100.0f * 0.001f))) { + this->targetRotY = -((Rand_ZeroFloat(0.4f) + 0.6f) * 1536.0f); + } else if ((this->runOffsetRot == this->targetRotY) && (Rand_ZeroOne() < 0.005f)) { + if (this->targetRotY > 0) { + this->targetRotY = -((Rand_ZeroFloat(0.4f) + 0.6f) * 1536.0f); + } else { + this->targetRotY = ((Rand_ZeroFloat(0.4f) + 0.6f) * 1536.0f); + } + } + + Math_SmoothStepToS(&this->runOffsetRot, this->targetRotY, 5, 0x800, 0x10); + Math_SmoothStepToS(&this->actor.shape.rot.y, this->baseRotY + this->runOffsetRot, 5, 0x800, 0x10); + + yawDiff = this->actor.yawTowardsPlayer - this->actor.shape.rot.y; + this->actor.world.rot.y = this->actor.shape.rot.y; + if ((this->lightningHitSomething != true) && (this->actor.xzDistToPlayer > 750.0f) && + (this->actor.xzDistToPlayer < 1650.0f) && (ABS_ALT(yawDiff) < 0x3000)) { + BossHakugin_SetupCharge(this); + } + } +} + +void BossHakugin_SetupCharge(BossHakugin* this) { + Animation_Change(&this->skelAnime, &gGohtRunAnim, 1.5f, 0.0f, 0.0f, ANIMMODE_LOOP, -3.0f); + this->actionFunc = BossHakugin_Charge; +} + +/** + * This function makes Goht lower its head and charge quickly towards the player. If Goht hits the player, or if Goht + * misses and the player ends up behind it, then this function will transition Goht back to running. + */ +void BossHakugin_Charge(BossHakugin* this, PlayState* play) { + s16 angle; + + Math_ScaledStepToS(&this->frontHalfRotZ, 0x800, 0x100); + Math_StepToF(&this->actor.speed, 25.0f, 2.0f); + SkelAnime_Update(&this->skelAnime); + BossHakugin_RunUpdateCommon(this, play); + + if ((this->transformedPlayerPos.z < 0.0f) || (this->bodyCollider.base.atFlags & AT_HIT)) { + BossHakugin_SetupRun(this); + } else { + if ((this->distToLeftWall < (89100.0f * 0.001f)) || (this->distToRightWall < (89100.0f * 0.001f))) { + this->targetRotY = this->baseRotY; + } else { + if (((this->transformedPlayerPos.x > 0.0f) && (this->transformedPlayerPos.x < this->distToLeftWall)) || + ((this->transformedPlayerPos.x <= 0.0f) && (-this->transformedPlayerPos.x < this->distToRightWall))) { + angle = this->actor.yawTowardsPlayer - this->actor.shape.rot.y; + angle = CLAMP(angle, -0x3000, 0x3000); + this->targetRotY = this->actor.shape.rot.y + angle; + } else { + this->targetRotY = + ((s16)(this->actor.yawTowardsPlayer - this->actor.shape.rot.y) >> 1) + this->actor.shape.rot.y; + } + } + + angle = this->targetRotY - this->baseRotY; + if (angle > 0x1800) { + this->targetRotY = this->baseRotY + 0x1800; + } else if (angle < -0x1800) { + this->targetRotY = this->baseRotY - 0x1800; + } + + Math_SmoothStepToS(&this->actor.shape.rot.y, this->targetRotY, 5, 0x800, 0x100); + this->actor.world.rot.y = this->actor.shape.rot.y; + } +} + +void BossHakugin_SetupDowned(BossHakugin* this) { + Animation_MorphToPlayOnce(&this->skelAnime, &gGohtFallDownAnim, -3.0f); + this->bodyCollider.base.atFlags &= ~AT_ON; + this->bodyCollider.base.acFlags &= ~AC_HARD; + this->frontHalfRotZ = 0; + this->finishedFallingDown = false; + this->chargingLightOrbScale = 0.0f; + + if (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_CHARGE) { + Math_Vec3f_Copy(&this->electricBallPos[0], &this->chargingLightningPos); + this->electricBallSpeed = this->actor.speed + 100.0f; + this->electricBallRot.x = Math_CosS(0xA00) * Math_SinS(this->actor.shape.rot.y); + this->electricBallRot.y = Math_SinS(0xA00); + this->electricBallRot.z = Math_CosS(0xA00) * Math_CosS(this->actor.shape.rot.y); + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_FLY_FORWARD; + this->chargingLightOrbScale = 0.0f; + Audio_PlaySfx_AtPos(&this->sfxPos, NA_SE_EN_COMMON_E_BALL_THR); + this->electricBallCollider.base.atFlags |= AT_ON; + } + + this->timer = 60; + this->actor.speed = 20.0f; + this->actionFunc = BossHakugin_Downed; +} + +/** + * This function will play Goht's falling down animation and make it skid to a complete stop. Once it stops, it will + * randomly shake in place for 60 frames, then it will get up and go back to running. + */ +void BossHakugin_Downed(BossHakugin* this, PlayState* play) { + CollisionPoly* floorPoly; + Vec3f velocity; + Vec3f pos; + s32 i; + s32 bgId; + + Actor_PlaySfx_Flagged(&this->actor, NA_SE_EN_COMMON_WEAKENED - SFX_FLAG); + + if (SkelAnime_Update(&this->skelAnime)) { + if (!this->finishedFallingDown) { + this->finishedFallingDown = true; + BossHakugin_RequestQuakeAndRumble(this, play, 17000, 7, 10); + } + + // This will spawn dust on Goht's sides as it skids to a stop. + if (this->actor.speed > 5.0f) { + for (i = -1; i < 2; i += 2) { + s16 angle = this->actor.shape.rot.y + (i * 0x7800); + f32 offset; + + velocity.x = 2.0f * Math_SinS(angle); + velocity.y = 1.0f; + velocity.z = 2.0f * Math_CosS(angle); + + angle = this->actor.shape.rot.y + (i * 0x4000); + offset = Rand_ZeroFloat(190.0f); + + pos.x = this->actor.world.pos.x + (80.0f * Math_SinS(angle)) + + (Math_SinS(this->actor.shape.rot.y) * offset); + pos.z = this->actor.world.pos.z + (80.0f * Math_CosS(angle)) + + (Math_CosS(this->actor.shape.rot.y) * offset); + pos.y = this->actor.world.pos.y + 300.0f; + pos.y = + BgCheck_EntityRaycastFloor5_2(play, &play->colCtx, &floorPoly, &bgId, &this->actor, &pos) + 10.0f; + + func_800B12F0(play, &pos, &velocity, &gZeroVec3f, Rand_S16Offset(750, 100), 10, 30); + } + } + } + + Math_SmoothStepToS(&this->actor.shape.rot.y, this->baseRotY, 5, 0x800, 0x10); + this->actor.world.rot.y = this->actor.shape.rot.y; + + if (this->actor.speed < 0.1f) { + this->timer--; + this->actor.world.pos.x = this->actor.home.pos.x + Rand_CenteredFloat(5.0f); + this->actor.world.pos.y = this->actor.home.pos.y + Rand_CenteredFloat(5.0f); + this->actor.world.pos.z = this->actor.home.pos.z + Rand_CenteredFloat(5.0f); + + if (this->timer == 0) { + this->bodyCollider.base.acFlags |= AC_HARD; + Math_Vec3f_Copy(&this->actor.world.pos, &this->actor.home.pos); + BossHakugin_SetupRun(this); + this->disableBodyCollidersTimer = 20; + } + } else if (Math_StepToF(&this->actor.speed, 5.0f, 0.3f)) { + Math_Vec3f_Copy(&this->actor.home.pos, &this->actor.world.pos); + this->actor.speed = 0.0f; + } +} + +void BossHakugin_SetupThrow(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + + Animation_MorphToPlayOnce(&this->skelAnime, &gGohtStationaryAnim, -10.0f); + player->actor.parent = &this->actor; + this->bodyCollider.base.atFlags &= ~AT_ON; + this->bodyCollider.base.ocFlags1 &= ~OC1_ON; + this->timer = 10; + this->actionFunc = BossHakugin_Throw; +} + +/** + * This function stops Goht in place and makes it rotate its front half upwards; the player is "attached" to Goht's head + * and is lifted into the air as Goht rotates. When Goht finishes its rotation, it throws the player backwards and waits + * 10 frames before running again. + */ +void BossHakugin_Throw(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + s16 angle; + f32 cos; + + SkelAnime_Update(&this->skelAnime); + Math_StepToF(&this->actor.speed, 0.0f, 2.0f); + Math_SmoothStepToS(&this->actor.shape.rot.y, this->baseRotY, 5, 0x800, 0x100); + this->actor.world.rot.y = this->actor.shape.rot.y; + + if (this->timer < 10) { + this->timer--; + if (this->timer == 0) { + this->bodyCollider.base.atFlags |= AT_ON; + this->bodyCollider.base.ocFlags1 |= OC1_ON; + BossHakugin_SetupRun(this); + } + } else if (Math_ScaledStepToS(&this->frontHalfRotZ, -0x800, 0x1C0)) { + player->av2.actionVar2 = 100; + player->actor.parent = NULL; + player->invincibilityTimer = 0; + player->actor.shape.rot.x = 0; + func_800B8D50(play, &this->actor, 10.0f, (this->actor.shape.rot.y + 0x8000), 15.0f, 0x10); + player->actor.world.pos.y += 30.0f; + this->timer--; + } else { + player->av2.actionVar2 = 0; + angle = -this->headRot.z + 0x1F40; + player->actor.shape.rot.x = -this->headRot.z + 0x8FC0; + + cos = Math_CosS(angle); + player->actor.world.pos.x = this->actor.focus.pos.x - (Math_SinS(this->baseRotY) * (20.0f * cos)); + player->actor.world.pos.y = this->actor.focus.pos.y - (Math_SinS(angle) * 20.0f); + player->actor.world.pos.z = this->actor.focus.pos.z - (Math_CosS(this->baseRotY) * (20.0f * cos)); + } +} + +void BossHakugin_SetupWait(BossHakugin* this, PlayState* play) { + f32 direction; + s16 atan; + + if (this->actionFunc != BossHakugin_ShootLightning) { + Animation_MorphToPlayOnce(&this->skelAnime, &gGohtStationaryAnim, -5.0f); + this->actor.speed = 0.0f; + atan = Math_Atan2S_XY(this->actor.world.pos.z, this->actor.world.pos.x); + + if (atan < -0x4000) { + this->actor.world.pos.x = -1200.0f; + this->actor.shape.rot.y = 0x4000; + this->actor.world.pos.z = -1200.0f; + } else if (atan < 0) { + this->actor.world.pos.x = -1200.0f; + this->actor.shape.rot.y = -0x8000; + this->actor.world.pos.z = 1200.0f; + } else if (atan < 0x4000) { + this->actor.world.pos.x = 1200.0f; + this->actor.shape.rot.y = -0x4000; + this->actor.world.pos.z = 1200.0f; + } else { + this->actor.world.pos.x = 1200.0f; + this->actor.shape.rot.y = 0; + this->actor.world.pos.z = -1200.0f; + } + + if (this->direction == GOHT_DIRECTION_CLOCKWISE) { + direction = -1.0f; + this->actor.shape.rot.y -= 0x4000; + } else { + direction = 1.0f; + } + + this->baseRotY = this->actor.shape.rot.y + (this->direction * 0x6000); + this->actor.world.rot.y = this->actor.shape.rot.y; + this->actor.world.pos.x += direction * 300.0f * Math_CosS(this->actor.shape.rot.y); + this->actor.world.pos.z -= direction * 300.0f * Math_SinS(this->actor.shape.rot.y); + this->actor.world.pos.y += 500.0f; + this->actor.world.pos.y = + BgCheck_EntityRaycastFloor1(&play->colCtx, &this->actor.floorPoly, &this->actor.world.pos); + this->lightningHitSomething = false; + } + + this->frontHalfRotZ = 0x700; + this->timer = 150; + this->chargingLightningTranslateZ = -100.0f; + this->actionFunc = BossHakugin_Wait; +} + +/** + * This function makes Goht wait while facing the direction opposite of the direction in which it was last running. If + * the player gets in range in front of Goht, it will stop waiting and start shooting lightning. Goht will stop waiting + * and start running in its original direction if the player gets behind Goht, if the player is too close to Goht, or if + * the player gets hit by one of Goht's lightning attacks. If Goht is still waiting after 150 frames, then it will + * reverse its direction and run towards the player. + */ +void BossHakugin_Wait(BossHakugin* this, PlayState* play) { + f32 absTransformedPlayerPosX; + + SkelAnime_Update(&this->skelAnime); + absTransformedPlayerPosX = fabsf(this->transformedPlayerPos.x); + Math_StepToF(&this->chargingLightOrbScale, 0.0f, 6.0f); + + if ((this->transformedPlayerPos.z < 0.0f) || (this->actor.xzDistToPlayer < 750.0f) || + (this->lightningHitSomething == true)) { + BossHakugin_SetupRun(this); + return; + } + + if ((this->lightningSegments[0].alpha == 0) && (this->chargingLightOrbScale < 0.1f) && + (absTransformedPlayerPosX < 400.0f) && (this->transformedPlayerPos.z > 0.0f)) { + BossHakugin_SetupShootLightning(this); + return; + } + + if ((absTransformedPlayerPosX > 400.0f) && ((play->gameplayFrames & 15) == 15)) { + BossHakugin_SpawnStalactite(this); + return; + } + + if (this->timer > 0) { + this->timer--; + } else { + this->direction = -this->direction; + this->baseRotY = this->actor.shape.rot.y; + this->actor.world.rot.y = this->actor.shape.rot.y; + BossHakugin_SetupRun(this); + } +} + +void BossHakugin_SetupShootLightning(BossHakugin* this) { + this->chargeUpTimer = 5; + this->chargingLightOrbScale = 0.0f; + this->actionFunc = BossHakugin_ShootLightning; +} + +/** + * This function will make Goht charge up an attack and then fire a single burst of lightning. After this, it will go + * back to waiting. + */ +void BossHakugin_ShootLightning(BossHakugin* this, PlayState* play) { + SkelAnime_Update(&this->skelAnime); + Audio_PlaySfx_AtPos(&this->sfxPos, NA_SE_EN_COMMON_THUNDER - SFX_FLAG); + + if (BossHakugin_ChargeUpAttack(this)) { + BossHakugin_AddLightningSegments(this, &this->chargingLightningPos, play); + BossHakugin_SetupWait(this, play); + } +} + +void BossHakugin_SetupCutsceneStart(BossHakugin* this) { + CutsceneManager_Queue(this->actor.csId); + if (this->actionFunc == BossHakugin_FrozenBeforeFight) { + this->nextCutsceneType = GOHT_NEXT_CUTSCENE_TYPE_INTRO; + } else { + this->nextCutsceneType = GOHT_NEXT_CUTSCENE_TYPE_ENTRANCE; + } + + this->actionFunc = BossHakugin_CutsceneStart; +} + +/** + * Starts either the entrance cutscene, the intro cutscene, or the death cutscene, depending on Goht's current action + * function and health. + */ +void BossHakugin_CutsceneStart(BossHakugin* this, PlayState* play) { + if (CutsceneManager_IsNext(this->actor.csId)) { + CutsceneManager_StartWithPlayerCs(this->actor.csId, &this->actor); + if (this->actor.colChkInfo.health == 0) { + BossHakugin_SetupDeathCutsceneRun(this); + } else if (this->nextCutsceneType == GOHT_NEXT_CUTSCENE_TYPE_INTRO) { + BossHakugin_SetupIntroCutsceneThaw(this); + } else { + BossHakugin_SetupEntranceCutscene(this, play); + } + } else { + CutsceneManager_Queue(this->actor.csId); + } +} + +void BossHakugin_SetupDeathCutsceneRun(BossHakugin* this) { + f32 direction; + + Animation_Change(&this->skelAnime, &gGohtRunAnim, 1.3f, 0.0f, 0.0f, ANIMMODE_LOOP_INTERP, -3.0f); + this->subCamId = CutsceneManager_GetCurrentSubCamId(this->actor.csId); + + if ((this->actor.world.pos.z >= 0.0f) && (this->actor.world.pos.x >= 0.0f)) { + this->targetRotY = -0x4000; + } else if ((this->actor.world.pos.z >= 0.0f) && (this->actor.world.pos.x < 0.0f)) { + this->targetRotY = -0x8000; + } else if ((this->actor.world.pos.x >= 0.0f) && (this->actor.world.pos.z < 0.0f)) { + this->targetRotY = 0; + } else { + this->targetRotY = 0x4000; + } + + if (this->direction == GOHT_DIRECTION_CLOCKWISE) { + this->targetRotY -= 0x4000; + } + + direction = this->direction; + if ((this->targetRotY > 0x6000) || (this->targetRotY < -0x6000)) { + this->subCamEye.x = direction * -1200.0f; + this->subCamEye.y = (direction * 80.0f) + -240.0f + 60.0f; + this->subCamEye.z = 1200.0f; + } else if ((this->targetRotY < 0x2000) && (this->targetRotY > -0x2000)) { + this->subCamEye.x = direction * 1200.0f; + this->subCamEye.y = 60.0f; + this->subCamEye.z = -1200.0f; + } else if (this->targetRotY >= 0x2000) { + this->subCamEye.x = -1200.0f; + this->subCamEye.y = (80.0f * direction) + -80.0f + 60.0f; + this->subCamEye.z = direction * -1200.0f; + } else { + this->subCamEye.x = 1200.0f; + this->subCamEye.y = (-160.0f * direction) + -160.0f + 60.0f; + this->subCamEye.z = direction * 1200.0f; + } + + this->chargingLightOrbScale = 0.0f; + this->frontHalfRotZ = 0; + Play_EnableMotionBlur(140); + this->runOffsetRot = Rand_S16Offset(0x800, 0x800) * ((Rand_ZeroOne() < 0.5f) ? -1 : 1); + SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 1); + this->actor.home.rot.y = this->actor.shape.rot.y; + this->timer = 18; + if ((this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FLY_FORWARD) || + (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FLY_TOWARDS_PLAYER)) { + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_FADE_OUT; + this->electricBallCount = 0; + } + + this->deathCutsceneRandomHeadRot = Rand_ZeroFloat(6144.0f); + this->headRot.y = 0; + this->headRot.z = 0; + this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; + this->actor.speed = CLAMP_MIN(this->actor.speed, 16.0f); + this->actionFunc = BossHakugin_DeathCutsceneRun; +} + +/** + * Handles the first part of Goht's death cutscene where Goht runs forward while turning side-to-side wildly. Once + * Goht's position and y-rotation reach a specific range of values, this function will make Goht swerve into the wall. + */ +void BossHakugin_DeathCutsceneRun(BossHakugin* this, PlayState* play) { + Vec3f subCamAt; + f32 absPosX; + f32 absPosZ; + s32 absRotY; + + Math_StepToF(&this->actor.speed, 15.0f, 2.0f); + SkelAnime_Update(&this->skelAnime); + + // Setting these to 10 prevents Goht from spawning boulders, stalactites, and bombs during the cutscene, since the + // relevant spawn functions are called fewer than 10 times during the cutscene. + this->preventBoulderSpawnCount = 10; + this->preventStalactiteSpawnCount = 10; + this->preventBombSpawnCount = 10; + + BossHakugin_RunUpdateCommon(this, play); + Math_SmoothStepToS(&this->actor.home.rot.y, this->baseRotY, 5, 0x800, 0x100); + this->timer--; + this->actor.shape.rot.y = + (s32)(Math_SinF(this->timer * (M_PI / 18.0f)) * this->runOffsetRot) + this->actor.home.rot.y; + + if (this->timer == 0) { + if (this->runOffsetRot > 0) { + this->runOffsetRot = -Rand_S16Offset(0x800, 0x800); + } else { + this->runOffsetRot = Rand_S16Offset(0x800, 0x800); + } + + this->timer = 18; + this->deathCutsceneRandomHeadRot = Rand_ZeroFloat(6144.0f); + } + + this->headRot.z = (8192.0f * Math_SinS(this->deathCutsceneRandomHeadRot)) * Math_SinF(this->timer * (M_PI / 9)); + this->headRot.y = (8192.0f * Math_CosS(this->deathCutsceneRandomHeadRot)) * Math_SinF(this->timer * (M_PI / 9)); + + this->actor.shape.rot.z = Math_SinF(this->timer * (M_PI / 18)) * -(this->runOffsetRot * 0.3f); + this->actor.world.rot.y = this->actor.shape.rot.y; + + subCamAt.x = (Math_SinS(this->actor.shape.rot.y) * 100.0f) + this->actor.world.pos.x; + subCamAt.y = this->actor.world.pos.y + 200.0f; + subCamAt.z = (Math_CosS(this->actor.shape.rot.y) * 100.0f) + this->actor.world.pos.z; + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &this->subCamEye); + + if (this->targetRotY == this->baseRotY) { + absPosX = fabsf(this->actor.world.pos.x); + absPosZ = fabsf(this->actor.world.pos.z); + absRotY = ABS_ALT(this->actor.shape.rot.y); + + if (((absPosX > 1289.1f) && (absPosZ < 1110.9f) && ((absRotY < 0x2000) || (absRotY > 0x6000))) || + ((absPosZ > 1289.1f) && (absPosX < 1110.9f) && (absRotY > 0x2000) && (absRotY < 0x6000))) { + BossHakugin_SetupDeathCutsceneSwerveIntoWall(this); + } + } +} + +void BossHakugin_SetupDeathCutsceneSwerveIntoWall(BossHakugin* this) { + if (this->direction == GOHT_DIRECTION_CLOCKWISE) { + this->targetRotY -= 0x4000; + } else { + this->targetRotY += 0x4000; + } + + this->actor.speed = 15.0f; + this->actionFunc = BossHakugin_DeathCutsceneSwerveIntoWall; +} + +/** + * Handles the second part of Goht's death cutscene where it swerves hard to the side and slams into the wall. Once + * Goht's y-rotation matches the target rotation, this function will transition Goht into being crushed by rocks. + */ +void BossHakugin_DeathCutsceneSwerveIntoWall(BossHakugin* this, PlayState* play) { + Vec3f subCamAt; + Vec3f eyeTarget; + s32 absTargetRotY; + s16 eyeTargetAngle; + + SkelAnime_Update(&this->skelAnime); + + subCamAt.x = (Math_SinS(this->actor.shape.rot.y) * 100.0f) + this->actor.world.pos.x; + subCamAt.y = this->actor.world.pos.y + 200.0f; + subCamAt.z = (Math_CosS(this->actor.shape.rot.y) * 100.0f) + this->actor.world.pos.z; + + eyeTargetAngle = this->actor.shape.rot.y - (this->direction * 0x6000); + eyeTarget.x = (Math_SinS(eyeTargetAngle) * 400.0f) + this->actor.world.pos.x; + eyeTarget.y = this->actor.world.pos.y + 100.0f; + eyeTarget.z = (Math_CosS(eyeTargetAngle) * 400.0f) + this->actor.world.pos.z; + + Math_Vec3f_StepTo(&this->subCamEye, &eyeTarget, 25.0f); + Play_SetCameraAtEye(play, this->subCamId, &subCamAt, &this->subCamEye); + + // Setting these to 10 prevents Goht from spawning boulders, stalactites, and bombs during the cutscene, since the + // relevant spawn functions are called fewer than 10 times during the cutscene. + this->preventBoulderSpawnCount = 10; + this->preventStalactiteSpawnCount = 10; + this->preventBombSpawnCount = 10; + + BossHakugin_RunUpdateCommon(this, play); + + if (Math_ScaledStepToS(&this->actor.shape.rot.y, this->targetRotY, 0x300) && + ((this->distToRightWall <= 189.00002f) || (this->distToLeftWall <= 189.00002f))) { + BossHakugin_SetupDeathCutsceneCrushedByRocks(this); + } else if (((this->direction == GOHT_DIRECTION_CLOCKWISE) && (this->distToRightWall <= 189.00002f)) || + ((this->direction == GOHT_DIRECTION_COUNTERCLOCKWISE) && (this->distToLeftWall <= 189.00002f))) { + absTargetRotY = ABS_ALT(this->targetRotY); + if (absTargetRotY < 0x2000) { + this->actor.world.pos.z = -1389.0f; + } else if (absTargetRotY > 0x6000) { + this->actor.world.pos.z = 1389.0f; + } else if (this->targetRotY > 0) { + this->actor.world.pos.x = -1389.0f; + } else { + this->actor.world.pos.x = 1389.0f; + } + } + + this->actor.world.rot.y = this->actor.shape.rot.y; +} + +void BossHakugin_SetupDeathCutsceneCrushedByRocks(BossHakugin* this) { + this->timer = 0; + this->actor.speed = 0.0f; + this->skelAnime.playSpeed = 0.5f; + BossHakugin_SpawnCrushingRocks(this); + this->actionFunc = BossHakugin_DeathCutsceneCrushedByRocks; +} + +typedef struct { + /* 0x0 */ GohtColliderBodyPart colliderIndex; + /* 0x4 */ u32 limbHideFlags; +} ExplosionLimbHideInfo; // size = 0x8 + +/** + * This array is used to determine the timing of when certain rocks should fall to crush Goht. Rocks fall in four + * distinct waves, and which rocks fall in which wave is dependent on the values in this array. For example, for the + * first wave, rocks with an index of 0 to 14 all fall at once; for the second wave, rocks with an index of 15 to 25 all + * fall at once. You can see how this array is used in `BossHakugin_DeathCutsceneCrushedByRocks` for more information. + */ +static s32 sRockNums[5] = { 0, 15, 26, 33, 36 }; + +/** + * Various explosions go off as rocks fall to crush Goht. Each element of this array is used to determine: + * - At which body part collider a given explosion should originate + * - Which limbs should be hidden when an explosion occurs. This is necessary to do because Goht will still be + * performing its running animation as it gets crushed; if its limbs were not hidden, the player would be able + * to see them sticking out of the rocks. + */ +static ExplosionLimbHideInfo sExplosionLimbHideInfo[] = { + { GOHT_COLLIDER_BODYPART_HEAD, GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_HEAD) | GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_JAW) }, + { GOHT_COLLIDER_BODYPART_FRONT_RIGHT_LOWER_LEG, GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_FRONT_RIGHT_UPPER_LEG) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_FRONT_RIGHT_LOWER_LEG) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_FRONT_RIGHT_HOOF) }, + { GOHT_COLLIDER_BODYPART_FRONT_LEFT_LOWER_LEG, GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_FRONT_LEFT_UPPER_LEG) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_FRONT_LEFT_LOWER_LEG) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_FRONT_LEFT_HOOF) }, + { GOHT_COLLIDER_BODYPART_BACK_LEFT_SHIN, GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_LEFT_SHIN) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_LEFT_PASTERN) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_LEFT_HOOF) }, + { GOHT_COLLIDER_BODYPART_BACK_RIGHT_SHIN, GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_RIGHT_SHIN) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_RIGHT_PASTERN) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_RIGHT_HOOF) }, + { GOHT_COLLIDER_BODYPART_THORAX, GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_PELVIS) | GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_THORAX) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_RIGHT_THIGH) | + GOHT_LIMB_DRAW_FLAG(GOHT_LIMB_BACK_LEFT_THIGH) }, +}; + +/** + * Handles the final part of Goht's death cutscene where rocks fall from the ceiling to crush it. As the rocks cover + * Goht's body, this function will set off various explosions and hide Goht's limbs. After 30 frames, this function will + * start the "boss defeated" theme, and after 180 frames, it will end the cutscene, spawn the heart container and + * blue warp, enable collision on the fallen rocks, and stop running Goht's regular update and draw functions. + */ +void BossHakugin_DeathCutsceneCrushedByRocks(BossHakugin* this, PlayState* play) { + EnBom* bomb; + Vec3s* pos; + Camera* subCam = Play_GetCamera(play, this->subCamId); + Vec3f eyeTarget; + s16 eyeTargetAngle; + s32 index; + ExplosionLimbHideInfo* explosionLimbHideInfo; + + SkelAnime_Update(&this->skelAnime); + index = this->timer / 6; + + eyeTargetAngle = this->actor.shape.rot.y + (0x6000 * this->direction); + eyeTarget.x = (Math_SinS(eyeTargetAngle) * 500.0f) + this->actor.world.pos.x; + eyeTarget.y = this->actor.world.pos.y + 250.0f; + eyeTarget.z = (Math_CosS(eyeTargetAngle) * 500.0f) + this->actor.world.pos.z; + Math_Vec3f_StepTo(&this->subCamEye, &eyeTarget, 25.0f); + Play_SetCameraAtEye(play, this->subCamId, &subCam->at, &this->subCamEye); + + if (index == 30) { + Camera* mainCam = Play_GetCamera(play, CAM_ID_MAIN); + + Play_DisableMotionBlur(); + CutsceneManager_Stop(this->actor.csId); + Play_SetCameraAtEye(play, this->subCamId, &mainCam->at, &mainCam->eye); + BossHakugin_SpawnBlueWarpAndHeartContainer(this, play); + BossHakugin_UpdateCrushingRocksCollision(this); + this->actor.draw = BossHakugin_DrawCrushingRocks; + this->actor.update = BossHakugin_UpdateStationaryCrushingRocks; + } else if (((this->timer % 6) == 0) && (index < 6)) { + explosionLimbHideInfo = &sExplosionLimbHideInfo[index]; + pos = &this->bodyCollider.elements[explosionLimbHideInfo->colliderIndex].dim.worldSphere.center; + bomb = (EnBom*)Actor_Spawn(&play->actorCtx, play, ACTOR_EN_BOM, pos->x, pos->y, pos->z, + BOMB_EXPLOSIVE_TYPE_POWDER_KEG, 0, 0, BOMB_TYPE_BODY); + + if (bomb != NULL) { + bomb->timer = 0; + } + + this->limbDrawFlags &= ~explosionLimbHideInfo->limbHideFlags; + if (index == 5) { + SEQCMD_PLAY_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 0, NA_BGM_CLEAR_BOSS | SEQ_FLAG_ASYNC); + } + } + + if (((this->timer + 3) % 6) == 0) { + s32 i; + + index = ((this->timer + 3) / 6) - 1; + if (index < ARRAY_COUNT(sRockNums) - 1) { + for (i = sRockNums[index]; i < sRockNums[index + 1]; i++) { + this->crushingRocks[i].fallingSpeed = Rand_ZeroFloat(5.0f) + 5.0f; + } + } + + if (index < 6) { + if ((index % 2) != 0) { + Audio_PlaySfx_AtPos(&this->sfxPos, NA_SE_EN_LAST1_DEMO_WALL); + } else { + Audio_PlaySfx_AtPos(&this->sfxPos, NA_SE_EN_LAST1_DEMO_BREAK); + } + } + } + + this->timer++; +} + +/** + * This function checks to see if any of Goht's body colliders hit the player. If Goht is charging and the player + * touched Goht's head collider, this function can throw them. Otherwise, if the player doesn't currently have Goron + * Spikes and didn't block the collider with their shield, then this function will knock the player back. + */ +void BossHakugin_CheckForBodyColliderHit(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + + if ((this->bodyCollider.base.atFlags & AT_HIT) && + ((this->actionFunc == BossHakugin_Charge) || !(player->stateFlags3 & PLAYER_STATE3_80000))) { + if ((this->actionFunc == BossHakugin_Charge) && + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_HEAD].info.toucherFlags & TOUCH_HIT) && + !(this->bodyCollider.base.atFlags & AT_BOUNCED) && play->grabPlayer(play, player)) { + BossHakugin_SetupThrow(this, play); + } else if (player->stateFlags3 & PLAYER_STATE3_1000) { + //! @bug This block is unreachable. This is trying to check if the player curled up as a Goron without + //! spikes, but if the player gets hit in this state, they will be forcibly uncurled before this function + //! gets called; it's impossible to have the state flags necessary to end up here in the final game. + player->unk_B08 = player->linearVelocity = -5.0f; + player->unk_B0C += player->unk_B08 * 0.05f; + player->actor.velocity.y = 10.0f; + player->unk_B8C = 4; + player->actor.shape.rot.y = player->actor.home.rot.y = player->currentYaw = player->actor.world.rot.y; + } else if (!(this->bodyCollider.base.atFlags & AT_BOUNCED)) { + s16 knockbackYaw = this->actor.yawTowardsPlayer - this->actor.shape.rot.y; + + if (knockbackYaw > 0) { + knockbackYaw = this->actor.shape.rot.y + (s32)(knockbackYaw / 2.0f) + 0x4000; + } else { + knockbackYaw = this->actor.shape.rot.y + (s32)(knockbackYaw / 2.0f) - 0x4000; + } + + func_800B8D50(play, &this->actor, 5.0f, knockbackYaw, 6.0f, 0); + } + } +} + +/** + * Checks if Goht has been hit by an attack and updates its health if it has been hit. This function is also responsible + * for starting Goht's death cutscene when its health reaches 0. Returns true if Goht has been hit by an attack. + */ +s32 BossHakugin_UpdateDamage(BossHakugin* this, PlayState* play) { + if (this->bodyCollider.base.acFlags & AC_HIT) { + s32 i; + + for (i = 0; i < GOHT_COLLIDER_BODYPART_MAX; i++) { + if (this->bodyCollider.elements[i].info.bumperFlags & BUMP_HIT) { + break; + } + } + + if (i == GOHT_COLLIDER_BODYPART_MAX) { + return false; + } + + // DMG_DEKU_NUT | DMG_DEKU_STICK | DMG_ZORA_BOOMERANG | DMG_NORMAL_ARROW | DMG_HOOKSHOT | DMG_ICE_ARROW + // | DMG_LIGHT_ARROW | DMG_DEKU_SPIN | DMG_DEKU_BUBBLE | DMG_DEKU_LAUNCH | DMG_ZORA_BARRIER + if ((this->drawDmgEffType == ACTOR_DRAW_DMGEFF_FROZEN_NO_SFX) && + (this->bodyCollider.elements[i].info.acHitInfo->toucher.dmgFlags & 0x000DB0B3)) { + return false; + } + + BossHakugin_Thaw(this, play); + + if (this->actionFunc == BossHakugin_Downed) { + Actor_SetColorFilter(&this->actor, COLORFILTER_COLORFLAG_RED, 255, COLORFILTER_BUFFLAG_OPA, 15); + BossHakugin_UpdateDrawDmgEffect(this, play, i); + + if (Actor_ApplyDamage(&this->actor) == 0) { + Enemy_StartFinishingBlow(play, &this->actor); + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_DEAD_OLD); + BossHakugin_SetupCutsceneStart(this); + } else { + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_DAMAGE_OLD); + } + + this->disableBodyCollidersTimer = 15; + return true; + } + + if (((this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_GORON_SPIKES) || + (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_NONE) || + (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_FIRE) || + (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_FREEZE) || + (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_LIGHT_ORB) || + (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_BLUE_LIGHT_ORB) || + (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_EXPLOSIVE)) && + ((this->actionFunc == BossHakugin_Run) || (this->actionFunc == BossHakugin_ShootLightning) || + (this->actionFunc == BossHakugin_Wait) || (this->actionFunc == BossHakugin_Charge))) { + Player* player = GET_PLAYER(play); + + if (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_GORON_SPIKES) { + player->pushedSpeed = 15.0f; + if ((s16)(this->actor.yawTowardsPlayer - this->actor.shape.rot.y) > 0) { + player->pushedYaw = this->actor.shape.rot.y + 0x4000; + } else { + player->pushedYaw = this->actor.shape.rot.y - 0x4000; + } + } + + this->disableBodyCollidersTimer = 15; + Actor_SetColorFilter(&this->actor, COLORFILTER_COLORFLAG_RED, 255, COLORFILTER_BUFFLAG_OPA, 15); + this->damagedSpeedUpCounter += 35; + BossHakugin_UpdateDrawDmgEffect(this, play, i); + this->actor.colChkInfo.damage = this->bodyCollider.elements[i].info.acHitInfo->toucher.damage; + + if (Actor_ApplyDamage(&this->actor) == 0) { + Enemy_StartFinishingBlow(play, &this->actor); + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_DEAD_OLD); + BossHakugin_SetupCutsceneStart(this); + } else { + if ((this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_EXPLOSIVE) || + ((this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_GORON_SPIKES) && + (this->actionFunc != BossHakugin_Charge) && + ((this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_HEAD].info.bumperFlags & BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_THORAX].info.bumperFlags & BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_PELVIS].info.bumperFlags & BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_FRONT_RIGHT_UPPER_LEG].info.bumperFlags & + BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_FRONT_LEFT_UPPER_LEG].info.bumperFlags & + BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_BACK_RIGHT_THIGH].info.bumperFlags & + BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_BACK_LEFT_THIGH].info.bumperFlags & + BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_RIGHT_HORN].info.bumperFlags & BUMP_HIT) || + (this->bodyCollider.elements[GOHT_COLLIDER_BODYPART_LEFT_HORN].info.bumperFlags & BUMP_HIT)))) { + BossHakugin_SetupDowned(this); + } else if ((this->electricBallState == GOHT_ELECTRIC_BALL_STATE_NONE) && + (this->electricBallCount == 0) && (this->actionFunc == BossHakugin_Run) && + (this->actor.colChkInfo.damageEffect == GOHT_DMGEFF_GORON_SPIKES)) { + this->chargeUpTimer = 5; + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_CHARGE; + } + + Actor_PlaySfx(&this->actor, NA_SE_EN_ICEB_DAMAGE_OLD); + } + + return true; + } else { + // This block is for attacks with the effect of `GOHT_DMGEFF_DOWNED_ONLY` hitting Goht while it's standing + // upright. These attacks deal no damage and instead just spawn sparks and play a metal sound. + s32 j; + + this->disableBodyCollidersTimer = 20; + for (j = 0; j < ARRAY_COUNT(this->bodyColliderElements); j++) { + Vec3f hitPos; + ColliderInfo* colliderInfo = &this->bodyCollider.elements[j].info; + + if ((colliderInfo->bumperFlags & BUMP_HIT) && (colliderInfo->acHitInfo != NULL) && + !(colliderInfo->acHitInfo->toucherFlags & TOUCH_SFX_NONE)) { + Math_Vec3s_ToVec3f(&hitPos, &colliderInfo->bumper.hitPos); + EffectSsHitmark_SpawnFixedScale(play, EFFECT_HITMARK_METAL, &hitPos); + CollisionCheck_SpawnShieldParticlesMetalSound(play, &hitPos, &this->actor.projectedPos); + break; + } + } + } + } + + return false; +} + +/** + * Updates all of the various rock effects that Goht and EnHakurock can spawn. If the rock effect's timer is above zero, + * then it's y-velocity will be decreased to make it fall faster. Once the effect reaches a certain point below the + * floor, the timer is set to -1, effectively disabling the effect and allowing it to be reused later. + */ +void BossHakugin_UpdateRockEffects(BossHakugin* this) { + GohtRockEffect* rockEffect; + s32 i; + + for (i = 0; i < GOHT_ROCK_EFFECT_COUNT; i++) { + rockEffect = &this->rockEffects[i]; + + if (rockEffect->timer >= 0) { + rockEffect->timer--; + rockEffect->velocity.y += -1.0f; + Math_Vec3f_Sum(&rockEffect->pos, &rockEffect->velocity, &rockEffect->pos); + + if (rockEffect->pos.y < -500.0f) { + rockEffect->timer = -1; + } else { + rockEffect->rot.x += (s16)(0x700 + (Rand_Next() >> 0x17)); + rockEffect->rot.y += (s16)(0x900 + (Rand_Next() >> 0x17)); + rockEffect->rot.z += (s16)(0xB00 + (Rand_Next() >> 0x17)); + } + } + } +} + +/** + * Updates the rocks that crush Goht as they fall onto it during its death cutscene. The rocks gradually speed up as + * they fall until they approach their target position, at which point they completely stop moving. + */ +void BossHakugin_UpdateFallingCrushingRocks(BossHakugin* this) { + GohtCrushingRock* crushingRock; + s32 i; + + for (i = 0; i < GOHT_CRUSHING_ROCK_COUNT; i++) { + crushingRock = &this->crushingRocks[i]; + Math_StepToF(&crushingRock->pos.y, crushingRock->targetPosY, crushingRock->fallingSpeed); + if (crushingRock->fallingSpeed > 0.0f) { + crushingRock->fallingSpeed += 6.0f; + } + } +} + +/** + * Updates the black smoke component of the malfunction effect. The smoke will drift upwards and increase in scale; if + * the timer is below 0, the smoke will also decrease in alpha until it completely fades out. + */ +void BossHakugin_UpdateMalfunctionEffects(BossHakugin* this) { + s32 i; + s32 j; + + for (i = 0; i < ARRAY_COUNT(this->malfunctionEffects); i++) { + for (j = 0; j < ARRAY_COUNT(this->malfunctionEffects[0]); j++) { + GohtMalfunctionEffect* malfunctionEffect = &this->malfunctionEffects[i][j]; + + if (malfunctionEffect->alpha > 0) { + malfunctionEffect->timer--; + malfunctionEffect->pos.y += 3.5f; + malfunctionEffect->scaleXY += 0.003f; + + if (malfunctionEffect->timer < 0) { + malfunctionEffect->alpha -= 15; + if (malfunctionEffect->alpha < 0) { + malfunctionEffect->alpha = 0; + } + } + } + } + } +} + +/** + * Updates the segments of the lightning attack that Goht can shoot at the player while it's waiting. Each segment's + * alpha is decreased by 20 for every frame; if the alpha is below 60, the lightning segment will no longer be able to + * hurt the player. + */ +void BossHakugin_UpdateLightningSegments(BossHakugin* this, PlayState* play) { + GohtLightningSegment* lightningSegment; + s16 rand; + s32 i; + + for (i = 0; i < GOHT_LIGHTNING_SEGMENT_COUNT; i++) { + lightningSegment = &this->lightningSegments[i]; + + if (lightningSegment->collider.base.atFlags & AT_HIT) { + Player* player = GET_PLAYER(play); + + this->lightningHitSomething = true; + Player_PlaySfx(player, NA_SE_EN_COMMON_E_BALL_HIT); + lightningSegment->collider.base.atFlags &= ~AT_HIT; + } + + if (lightningSegment->alpha > 0) { + lightningSegment->alpha -= 20; + + if (lightningSegment->alpha <= 0) { + lightningSegment->alpha = 0; + } else if (lightningSegment->alpha <= 255) { + rand = (s32)Rand_Next() >> 0x10; + lightningSegment->rot.z = rand; + + if (lightningSegment->alpha > 60) { + CollisionCheck_SetAT(play, &play->colChkCtx, &lightningSegment->collider.base); + } + } + } + } +} + +/** + * Updates the electric balls that Goht can shoot at the player. + */ +void BossHakugin_UpdateElectricBalls(BossHakugin* this, PlayState* play) { + Player* player = GET_PLAYER(play); + CollisionPoly* poly = NULL; + Vec3f resultPos; + Vec3f posB; + Vec3f normal; + Vec3f targetRot; + Vec3f* firstBallPos; + Vec3f* ballPosIter; + s32 pad; + f32 angleStep; + s32 bgId; + s32 playedHitSfx = false; + + firstBallPos = &this->electricBallPos[0]; + + if (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FLY_FORWARD) { + if (Math_StepToF(&this->electricBallSpeed, 50.0f, 3.5f)) { + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_FLY_TOWARDS_PLAYER; + } + } else if (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FLY_TOWARDS_PLAYER) { + angleStep = + BINANG_TO_RAD(Math3D_Vec3fDistSq(firstBallPos, &player->actor.world.pos) * (1.0f / SQ(120.0f)) * 32.0f); + angleStep = CLAMP_MAX(angleStep, M_PI / 4.0f); + + Math_Vec3f_Diff(&player->actor.world.pos, firstBallPos, &targetRot); + + if (BossHakugin_Vec3fNormalize(&targetRot)) { + BossHakugin_StepVector(&this->electricBallRot, &targetRot, angleStep); + } + } else if (this->electricBallState != GOHT_ELECTRIC_BALL_STATE_FADE_OUT) { + return; + } + + Audio_PlaySfx_AtPos(&this->electricBallSfxPos, NA_SE_EN_COMMON_E_BALL - SFX_FLAG); + + if (this->electricBallCount < GOHT_ELECTRIC_BALL_COUNT_MAX) { + this->electricBallCount++; + } + + if ((this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FADE_OUT) && + (this->electricBallCount == GOHT_ELECTRIC_BALL_COUNT_MAX - 1)) { + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_NONE; + this->electricBallCount = 0; + } else { + // The electric ball attack is actually made up of a chain of light orbs; the first light orb is the one to + // which the collider is attached and the one that all other light orbs follow. This loop below will make it so + // that the position of `this->electricBallPos[N + 1]` will equal the position of `this->electricBallPos[N]` on + // the previous frame, creating a trail effect. + ballPosIter = &this->electricBallPos[GOHT_ELECTRIC_BALL_COUNT_MAX - 1]; + while (ballPosIter != firstBallPos) { + Math_Vec3f_Copy(ballPosIter, ballPosIter - 1); + ballPosIter--; + } + + this->lightOrbRotZ += (s16)(0x4000 + (Rand_Next() >> 0x12)); + + if (this->electricBallState != GOHT_ELECTRIC_BALL_STATE_FADE_OUT) { + if (this->electricBallCollider.base.atFlags & AT_HIT) { + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_FADE_OUT; + this->electricBallCount = 0; + this->electricBallCollider.base.atFlags &= ~AT_HIT; + this->electricBallCollider.base.atFlags &= ~AT_ON; + playedHitSfx = true; + Audio_PlaySfx_AtPos(&this->electricBallSfxPos, NA_SE_EN_COMMON_E_BALL_HIT); + } + + posB.x = firstBallPos->x + (this->electricBallSpeed * this->electricBallRot.x); + posB.y = firstBallPos->y + (this->electricBallSpeed * this->electricBallRot.y); + posB.z = firstBallPos->z + (this->electricBallSpeed * this->electricBallRot.z); + + if (BgCheck_EntityLineTest1(&play->colCtx, firstBallPos, &posB, &resultPos, &poly, true, true, true, true, + &bgId)) { + s16 yawDiff = false; + + Math_Vec3f_Copy(firstBallPos, &resultPos); + + if ((poly->normal.y > -0x6665) && (poly->normal.y < 0x3FFF)) { + yawDiff = Math_Vec3f_Yaw(firstBallPos, &player->actor.world.pos) - + Math_Atan2S_XY(COLPOLY_GET_NORMAL(poly->normal.z), COLPOLY_GET_NORMAL(poly->normal.x)); + yawDiff = (ABS_ALT(yawDiff) > 0x4000) ? true : false; + } + + if ((this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FLY_TOWARDS_PLAYER) && + (yawDiff || (poly->normal.y >= 0x4000))) { + this->electricBallState = GOHT_ELECTRIC_BALL_STATE_FADE_OUT; + this->electricBallCount = 0; + if (!playedHitSfx) { + Audio_PlaySfx_AtPos(&this->electricBallSfxPos, NA_SE_EN_COMMON_E_BALL_HIT); + } + } else { + normal.x = COLPOLY_GET_NORMAL(poly->normal.x); + normal.y = COLPOLY_GET_NORMAL(poly->normal.y); + normal.z = COLPOLY_GET_NORMAL(poly->normal.z); + func_80179F64(&this->electricBallRot, &normal, &targetRot); + Math_Vec3f_Copy(&this->electricBallRot, &targetRot); + } + } else { + Math_Vec3f_Copy(firstBallPos, &posB); + } + + this->electricBallCollider.dim.worldSphere.center.x = firstBallPos->x; + this->electricBallCollider.dim.worldSphere.center.y = firstBallPos->y; + this->electricBallCollider.dim.worldSphere.center.z = firstBallPos->z; + CollisionCheck_SetAT(play, &play->colChkCtx, &this->electricBallCollider.base); + } + } +} + +void BossHakugin_Update(Actor* thisx, PlayState* play) { + s32 pad; + BossHakugin* this = THIS; + Vec3s targetRot; + Player* player = GET_PLAYER(play); + + this->blockMalfunctionEffects = false; + DECR(this->damagedSpeedUpCounter); + + if (this->actionFunc != BossHakugin_CutsceneStart) { + if (!BossHakugin_UpdateDamage(this, play)) { + BossHakugin_CheckForBodyColliderHit(this, play); + } + } + + BossHakugin_UpdateBaseRot(this, play); + Actor_OffsetOfPointInActorCoords(&this->actor, &this->transformedPlayerPos, &player->actor.world.pos); + this->actionFunc(this, play); + Actor_MoveWithGravity(&this->actor); + Actor_UpdateBgCheckInfo(play, &this->actor, 450.0f, (89100.0f * 0.001f), 0.0f, + UPDBGCHECKINFO_FLAG_1 | UPDBGCHECKINFO_FLAG_4 | UPDBGCHECKINFO_FLAG_8 | + UPDBGCHECKINFO_FLAG_10); + func_800BE3D0(&this->actor, this->actor.shape.rot.y, &targetRot); + Math_ScaledStepToS(&this->actor.shape.rot.x, targetRot.x, 0x100); + Math_ScaledStepToS(&this->actor.shape.rot.z, targetRot.z, 0x100); + + if (this->actionFunc == BossHakugin_DeathCutsceneCrushedByRocks) { + BossHakugin_UpdateFallingCrushingRocks(this); + } else { + BossHakugin_UpdateRockEffects(this); + } + + BossHakugin_UpdateMalfunctionEffects(this); + BossHakugin_UpdateLightningSegments(this, play); + BossHakugin_UpdateElectricBalls(this, play); + BossHakugin_SpawnIceSparkle(this, play); + BossHakugin_ApplyGoronSpikeBoost(this, player); + BossHakugin_UpdateLight(this); + + if ((this->bodyCollider.base.atFlags & AT_ON) && (this->actor.colorFilterTimer == 0)) { + CollisionCheck_SetAT(play, &play->colChkCtx, &this->bodyCollider.base); + } else { + this->bodyCollider.base.atFlags &= ~AT_HIT; + } + + if (this->disableBodyCollidersTimer == 0) { + CollisionCheck_SetAC(play, &play->colChkCtx, &this->bodyCollider.base); + } else { + this->bodyCollider.base.acFlags &= ~AC_HIT; + this->disableBodyCollidersTimer--; + } + + if (this->bodyCollider.base.ocFlags1 & OC1_ON) { + CollisionCheck_SetOC(play, &play->colChkCtx, &this->bodyCollider.base); + } else { + this->bodyCollider.base.ocFlags1 &= ~OC1_HIT; + } + + if (this->drawDmgEffAlpha > 0.0f) { + if (this->drawDmgEffType != ACTOR_DRAW_DMGEFF_FROZEN_NO_SFX) { + Math_StepToF(&this->drawDmgEffAlpha, 0.0f, 0.05f); + this->drawDmgEffScale = (this->drawDmgEffAlpha + 1.0f) * 1.25f; + this->drawDmgEffScale = CLAMP_MAX(this->drawDmgEffScale, 2.5f); + } else if (!Math_StepToF(&this->drawDmgEffFrozenSteamScale, 2.5f, 5.0f / 12.0f)) { + Actor_PlaySfx_Flagged(&this->actor, NA_SE_EV_ICE_FREEZE - SFX_FLAG); + } else { + BossHakugin_Thaw(this, play); + } + } + + // The lowest valid `floorHeight` the player can have while still being within the bounds of the arena is -320.0f. + // Thus, this is checking if the player is below the floor; this entire block is about moving the player to a valid + // position if they've somehow left the arena. + if (player->actor.floorHeight < -400.0f) { + CollisionPoly* poly = NULL; + Vec3f resultPos; + Vec3f posA; + s32 bgId; + + if ((fabsf(player->actor.world.pos.z) < 1200.0f) && (fabsf(player->actor.world.pos.x) < 1200.0f)) { + s16 atan = Math_Atan2S_XY(player->actor.world.pos.z, player->actor.world.pos.x); + + posA.x = Math_SinS(atan) * 2000.0f; + posA.z = Math_CosS(atan) * 2000.0f; + } else { + if (player->actor.world.pos.z > 1200.0f) { + posA.z = 1500.0f; + } else if (player->actor.world.pos.z < -1200.0f) { + posA.z = -1500.0f; + } else { + posA.z = player->actor.world.pos.z; + } + if (player->actor.world.pos.x > 1200.0f) { + posA.x = 1500.0f; + } else if (player->actor.world.pos.x < -1200.0f) { + posA.x = -1500.0f; + } else { + posA.x = player->actor.world.pos.x; + } + } + + posA.y = 100.0f; + + if (BgCheck_EntityLineTest1(&play->colCtx, &posA, &player->actor.world.pos, &resultPos, &poly, true, true, + false, true, &bgId)) { + Math_Vec3f_Copy(&player->actor.world.pos, &resultPos); + Math_Vec3f_Copy(&player->actor.home.pos, &player->actor.world.pos); + Math_Vec3f_Copy(&player->actor.prevPos, &player->actor.world.pos); + player->actor.floorHeight = player->actor.world.pos.y; + player->actor.world.pos.y = player->actor.world.pos.y + 1.0f; + } else { + player->actor.world.pos.x = 0.0f; + player->actor.world.pos.y = 1.0f; + player->actor.world.pos.z = -1200.0f; + Math_Vec3f_Copy(&player->actor.home.pos, &player->actor.world.pos); + Math_Vec3f_Copy(&player->actor.prevPos, &player->actor.world.pos); + player->actor.floorHeight = player->actor.world.pos.y; + } + } +} + +s32 BossHakugin_OverrideLimbDraw(struct PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* pos, Vec3s* rot, + Actor* thisx) { + static s32 sCurrentMalfunctionType = GOHT_MALFUNCTION_NUM_TYPES - 1; + static s32 sCurrentLimbIndex = GOHT_LIMB_FRONT_RIGHT_UPPER_LEG; + BossHakugin* this = THIS; + + if (this->actionFunc == BossHakugin_DeathCutsceneCrushedByRocks) { + if (limbIndex == GOHT_LIMB_ROOT) { + pos->y -= this->actor.shape.yOffset; + } + + if (!(this->limbDrawFlags & GOHT_LIMB_DRAW_FLAG(limbIndex))) { + *dList = NULL; + } + } + + if (!this->blockMalfunctionEffects && (limbIndex == sCurrentLimbIndex)) { + Matrix_MultZero(&this->malfunctionEffects[sCurrentMalfunctionType][this->malfunctionEffectIndex].pos); + + sCurrentMalfunctionType--; + if (sCurrentMalfunctionType < 0) { + sCurrentMalfunctionType = GOHT_MALFUNCTION_NUM_TYPES - 1; + } + + sCurrentLimbIndex = sMalfunctionTypeToLimbIndex[sCurrentMalfunctionType]; + } + + if (limbIndex == GOHT_LIMB_HEAD) { + rot->z += this->frontHalfRotZ; + if (this->actionFunc == BossHakugin_DeathCutsceneRun) { + rot->z += this->headRot.z; + rot->y += this->headRot.y; + } + } else if (limbIndex == GOHT_LIMB_THORAX_WRAPPER) { + rot->z += (s16)(this->frontHalfRotZ * 2); + } else if ((limbIndex == GOHT_LIMB_FRONT_LEFT_LEG_ROOT) || (limbIndex == GOHT_LIMB_FRONT_RIGHT_LEG_ROOT)) { + rot->z -= (s16)(this->frontHalfRotZ * 3); + } + + return false; +} + +void BossHakugin_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Actor* thisx) { + BossHakugin* this = THIS; + s8 bodyPartIndex; + s32 i; + f32 cos; + s16 angle; + + Collider_UpdateSpheres(limbIndex, &this->bodyCollider); + bodyPartIndex = sLimbToBodyParts[limbIndex]; + + if (bodyPartIndex > BODYPART_NONE) { + for (i = 0; i < GOHT_COLLIDER_BODYPART_MAX; i++) { + if (this->bodyCollider.elements[i].dim.limb == limbIndex) { + Math_Vec3s_ToVec3f(&this->bodyPartsPos[bodyPartIndex], + &this->bodyCollider.elements[i].dim.worldSphere.center); + break; + } + } + + if (i == GOHT_COLLIDER_BODYPART_MAX) { + Matrix_MultZero(&this->bodyPartsPos[bodyPartIndex]); + } + } + + if (limbIndex == GOHT_LIMB_HEAD) { + Matrix_MultVecX(3500.0f, &this->actor.focus.pos); + this->actor.focus.rot.y = this->actor.shape.rot.y; + if (this->actionFunc != BossHakugin_DeathCutsceneRun) { + Matrix_MtxFToYXZRot(Matrix_GetCurrent(), &this->headRot, false); + } + + if (this->chargingLightOrbScale > 0.0f) { + angle = 0x12C0 - this->headRot.z; + cos = Math_CosS(angle); + this->chargingLightningPos.x = + this->actor.focus.pos.x - (Math_SinS(this->actor.shape.rot.y) * (60.0f * cos)); + this->chargingLightningPos.y = this->actor.focus.pos.y - (Math_SinS(angle) * 60.0f); + this->chargingLightningPos.z = + this->actor.focus.pos.z - (Math_CosS(this->actor.shape.rot.y) * (60.0f * cos)); + } + } +} + +/** + * Draws the various rock effects that Goht or EnHakurock can spawn. + */ +void BossHakugin_DrawRockEffects(BossHakugin* this, PlayState* play) { + GohtRockEffect* rockEffect; + s32 i; + + OPEN_DISPS(play->state.gfxCtx); + + gSPDisplayList(POLY_OPA_DISP++, gGohtRockMaterialDL); + for (i = 0; i < GOHT_ROCK_EFFECT_COUNT; i++) { + rockEffect = &this->rockEffects[i]; + if ((rockEffect->timer >= 0) && (rockEffect->type == GOHT_ROCK_EFFECT_TYPE_BOULDER)) { + Matrix_SetTranslateRotateYXZ(rockEffect->pos.x, rockEffect->pos.y, rockEffect->pos.z, &rockEffect->rot); + Matrix_Scale(rockEffect->scale, rockEffect->scale, rockEffect->scale, MTXMODE_APPLY); + gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_OPA_DISP++, gGohtRockModelDL); + } + } + + gSPDisplayList(POLY_OPA_DISP++, gGohtStalactiteMaterialDL); + for (i = 0; i < GOHT_ROCK_EFFECT_COUNT; i++) { + rockEffect = &this->rockEffects[i]; + if ((rockEffect->timer >= 0) && (rockEffect->type == GOHT_ROCK_EFFECT_TYPE_STALACTITE)) { + Matrix_SetTranslateRotateYXZ(rockEffect->pos.x, rockEffect->pos.y, rockEffect->pos.z, &rockEffect->rot); + Matrix_Scale(rockEffect->scale, rockEffect->scale, rockEffect->scale, MTXMODE_APPLY); + gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_OPA_DISP++, gGohtStalactiteModelDL); + } + } + + CLOSE_DISPS(play->state.gfxCtx); +} + +/** + * Draws the black smoke component of the malfunction effect. The electrical component is drawn by EffectSsFhgFlash. + */ +void BossHakugin_DrawMalfunctionEffects(BossHakugin* this, PlayState* play) { + GohtMalfunctionEffect* malfunctionEffect; + s32 i; + s32 j; + s16 camYaw; + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL25_Xlu(play->state.gfxCtx); + + gSPDisplayList(POLY_XLU_DISP++, gFrozenSteamMaterialDL); + camYaw = Camera_GetCamDirYaw(GET_ACTIVE_CAM(play)) + 0x8000; + gDPSetEnvColor(POLY_XLU_DISP++, 1, 1, 1, 128); + + for (i = 0; i < ARRAY_COUNT(this->malfunctionEffects); i++) { + for (j = 0; j < ARRAY_COUNT(this->malfunctionEffects[0]); j++) { + malfunctionEffect = &this->malfunctionEffects[i][j]; + if (malfunctionEffect->alpha > 0) { + gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, 0, 0, 0, malfunctionEffect->alpha); + gSPSegment(POLY_XLU_DISP++, 0x08, + Gfx_TwoTexScroll(play->state.gfxCtx, G_TX_RENDERTILE, malfunctionEffect->timer * 3, + malfunctionEffect->timer * 15, 32, 64, 1, 0, 0, 32, 32)); + Matrix_Translate(malfunctionEffect->pos.x, malfunctionEffect->pos.y, malfunctionEffect->pos.z, + MTXMODE_NEW); + Matrix_RotateYS(camYaw, MTXMODE_APPLY); + Matrix_Scale(malfunctionEffect->scaleXY, malfunctionEffect->scaleXY, 1.0f, MTXMODE_APPLY); + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), + G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_XLU_DISP++, gFrozenSteamModelDL); + } + } + } + + CLOSE_DISPS(play->state.gfxCtx); +} + +/** + * Draws the electricity and light orb that appears in front of Goht's head as it charges up certain attacks. + */ +void BossHakugin_DrawChargingLightning(BossHakugin* this, PlayState* play) { + s32 pad; + s32 i; + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL25_Xlu(play->state.gfxCtx); + + if (this->chargingLightOrbScale > 0.0f) { + gDPSetEnvColor(POLY_XLU_DISP++, sLightningColor.r, sLightningColor.g, sLightningColor.b, 0); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightningMaterialDL); + gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, 255, 255, 255, 255); + + for (i = -1; i <= 1; i += 2) { + Matrix_SetTranslateRotateYXZ(this->chargingLightningPos.x, this->chargingLightningPos.y, + this->chargingLightningPos.z, &this->headRot); + Matrix_RotateYS(0x1400 * i, MTXMODE_APPLY); + Matrix_RotateXS(0xC00 * i, MTXMODE_APPLY); + Matrix_RotateZS(this->lightOrbRotZ, MTXMODE_APPLY); + Matrix_Scale(0.62f, 0.62f, this->chargingLightningScale, MTXMODE_APPLY); + Matrix_Translate(0.0f, 0.0f, this->chargingLightningTranslateZ * i, MTXMODE_APPLY); + + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightningModelDL); + + Matrix_RotateZS(0x4000, MTXMODE_APPLY); + + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightningModelDL); + } + + gDPPipeSync(POLY_XLU_DISP++); + gDPSetEnvColor(POLY_XLU_DISP++, sLightColor.r, sLightColor.g, sLightColor.b, 0); + Matrix_Translate(this->chargingLightningPos.x, this->chargingLightningPos.y, this->chargingLightningPos.z, + MTXMODE_NEW); + Matrix_ReplaceRotation(&play->billboardMtxF); + Matrix_Scale(this->chargingLightOrbScale, this->chargingLightOrbScale, this->chargingLightOrbScale, + MTXMODE_APPLY); + Matrix_RotateZS(this->lightOrbRotZ, MTXMODE_APPLY); + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightOrbMaterialDL); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightOrbModelDL); + } + + CLOSE_DISPS(play->state.gfxCtx); +} + +/** + * Draws the segments of the lightning attack that Goht can shoot at the player while it's waiting. + */ +void BossHakugin_DrawLightningSegments(BossHakugin* this, PlayState* play) { + GohtLightningSegment* lightningSegment; + s32 i; + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL25_Xlu(play->state.gfxCtx); + + gDPSetEnvColor(POLY_XLU_DISP++, sLightningColor.r, sLightningColor.g, sLightningColor.b, 0); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightningMaterialDL); + + for (i = 0; i < GOHT_LIGHTNING_SEGMENT_COUNT; i++) { + lightningSegment = &this->lightningSegments[i]; + + if ((lightningSegment->alpha > 0) && (lightningSegment->alpha <= 255)) { + Matrix_SetTranslateRotateYXZ(lightningSegment->pos.x, lightningSegment->pos.y, lightningSegment->pos.z, + &lightningSegment->rot); + Matrix_Scale(1.0f, 1.0f, 1.0f, MTXMODE_APPLY); + + gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, 255, 255, 255, lightningSegment->alpha); + + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightningModelDL); + + Matrix_RotateZS(0x4000, MTXMODE_APPLY); + + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightningModelDL); + } + } + + CLOSE_DISPS(play->state.gfxCtx); +} + +/** + * Draws the electric balls that Goht can shoot at the player. + */ +void BossHakugin_DrawElectricBalls(BossHakugin* this, PlayState* play2) { + s32 pad[4]; + PlayState* play = play2; + s32 end; + f32 scale; + s32 i; + s16 rotZ; + s32 alpha; + Vec3f* pos; + + if (this->electricBallState == GOHT_ELECTRIC_BALL_STATE_FADE_OUT) { + i = GOHT_ELECTRIC_BALL_COUNT_MAX - 1; + end = this->electricBallCount; + } else if (this->electricBallCount != 0) { + i = this->electricBallCount - 1; + end = 0; + } else { + return; + } + + scale = 30.0f - ((i - 1) * 1.5f); + alpha = 215 - (i * 15); + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL25_Xlu(play->state.gfxCtx); + gDPSetEnvColor(POLY_XLU_DISP++, sLightColor.r, sLightColor.g, sLightColor.b, 0); + rotZ = this->lightOrbRotZ; + gSPDisplayList(POLY_XLU_DISP++, gGohtLightOrbMaterialDL); + + for (; i >= end; i--) { + pos = &this->electricBallPos[i]; + + gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, 255, 255, 255, alpha); + Matrix_Translate(pos->x, pos->y, pos->z, MTXMODE_NEW); + Matrix_ReplaceRotation(&play->billboardMtxF); + Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); + Matrix_RotateZS(rotZ, MTXMODE_APPLY); + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_XLU_DISP++, gGohtLightOrbModelDL); + + scale += 1.5f; + alpha += 15; + rotZ += 0x1000; + } + + CLOSE_DISPS(play->state.gfxCtx); +} + +/** + * Draws the ice that encases Goht before the fight begins. + */ +void BossHakugin_DrawIce(BossHakugin* this, PlayState* play) { + s32 pad; + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL25_Xlu(play->state.gfxCtx); + func_800B8118(&this->actor, play, 0); + Matrix_Translate(this->iceCollider.dim.pos.x, this->iceCollider.dim.pos.y, this->iceCollider.dim.pos.z, + MTXMODE_NEW); + Matrix_Scale(4.0f, this->iceScaleY, 4.0f, MTXMODE_APPLY); + Matrix_RotateYS(0xF00, MTXMODE_APPLY); + Matrix_RotateXS(0x500, MTXMODE_APPLY); + Matrix_RotateZS(-0x500, MTXMODE_APPLY); + + gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPSegment(POLY_XLU_DISP++, 0x08, + Gfx_TwoTexScroll(play->state.gfxCtx, G_TX_RENDERTILE, 0, play->gameplayFrames & 0xFF, 32, 16, 1, 0, + (play->gameplayFrames * 2) & 0xFF, 64, 32)); + gDPSetEnvColor(POLY_XLU_DISP++, 0, 50, 100, this->iceAlpha); + gSPDisplayList(POLY_XLU_DISP++, gEffIceFragment3DL); + + CLOSE_DISPS(play->state.gfxCtx); +} + +void BossHakugin_Draw(Actor* thisx, PlayState* play) { + s32 pad; + BossHakugin* this = THIS; + u8* tex = GRAPH_ALLOC(play->state.gfxCtx, GOHT_SHADOW_TEX_SIZE); + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL25_Opa(play->state.gfxCtx); + gSPSegment(POLY_OPA_DISP++, 0x08, sChinTexture); + SkelAnime_DrawFlexOpa(play, this->skelAnime.skeleton, this->skelAnime.jointTable, this->skelAnime.dListCount, + BossHakugin_OverrideLimbDraw, BossHakugin_PostLimbDraw, &this->actor); + + CLOSE_DISPS(play->state.gfxCtx); + + Actor_DrawDamageEffects(play, &this->actor, this->bodyPartsPos, GOHT_BODYPART_MAX, this->drawDmgEffScale, + this->drawDmgEffFrozenSteamScale, this->drawDmgEffAlpha, this->drawDmgEffType); + + SkinMatrix_Vec3fMtxFMultXYZ(&play->viewProjectionMtxF, &this->chargingLightningPos, &this->sfxPos); + SkinMatrix_Vec3fMtxFMultXYZ(&play->viewProjectionMtxF, &this->electricBallPos[0], &this->electricBallSfxPos); + + if (!this->blockMalfunctionEffects) { + BossHakugin_AddMalfunctionEffects(this, play); + } + + // Setting `blockMalfunctionEffects` to true here ensures that `BossHakugin_AddMalfunctionEffects` won't be called + // until `BossHakugin_Update` is called again, which is the only function that sets this to false. The player can + // call draw functions over and over without ever calling update functions by pausing and unpausing rapidly; by + // setting this to true, the developers ensured that the player couldn't add a potentially-infinite amount of + // malfunction effects by repeatedly pausing. + this->blockMalfunctionEffects = true; + + if (this->actor.colorFilterTimer != 0) { + func_800AE5A0(play); + } + + if (this->actionFunc == BossHakugin_DeathCutsceneCrushedByRocks) { + BossHakugin_DrawCrushingRocks(&this->actor, play); + } else { + BossHakugin_DrawRockEffects(this, play); + } + + BossHakugin_DrawMalfunctionEffects(this, play); + BossHakugin_DrawChargingLightning(this, play); + BossHakugin_DrawLightningSegments(this, play); + BossHakugin_DrawElectricBalls(this, play); + + if (this->iceAlpha > 0) { + BossHakugin_DrawIce(this, play); + } + + if (this->actionFunc != BossHakugin_DeathCutsceneCrushedByRocks) { + BossHakugin_GenShadowTex(tex, this); + BossHakugin_DrawShadowTex(tex, this, play); + } +} + +/** + * These four arrays encode circular shadow maps of various sizes. For an array of length N, the shadow map is N rows + * tall, and each entry in the array describes the start and end point of the shadow within a given row (the exact + * values of the start and end points are determined by the loops within BossHakugin_FillShadowTex). To illustrate using + * the sShadowSmallMap as an example: + * -3 -2 -1 0 1 + * ------------- + * 0 0 1 0 0 + * 0 1 1 1 0 + * 1 1 1 1 1 + * 1 1 1 1 1 + * 0 1 1 1 0 + * 0 0 1 0 0 + */ +static s32 sShadowSmallMap[] = { + 1, 2, 3, 3, 2, 1, +}; + +static s32 sShadowMediumMap[] = { + 2, 3, 4, 4, 4, 3, 2, +}; + +static s32 sShadowLargeMap[] = { + 2, 3, 4, 4, 4, 4, 3, 2, +}; + +static s32 sShadowExtraLargeMap[] = { + 2, 4, 5, 6, 7, 8, 8, 8, 8, 7, 6, 5, 4, 2, +}; + +static s32 sParentShadowBodyParts[GOHT_BODYPART_MAX] = { + GOHT_BODYPART_THORAX, // GOHT_BODYPART_PELVIS + BODYPART_NONE, // GOHT_BODYPART_THORAX + GOHT_BODYPART_THORAX, // GOHT_BODYPART_HEAD + GOHT_BODYPART_THORAX, // GOHT_BODYPART_FRONT_RIGHT_UPPER_LEG + GOHT_BODYPART_FRONT_RIGHT_UPPER_LEG, // GOHT_BODYPART_FRONT_RIGHT_LOWER_LEG + GOHT_BODYPART_FRONT_RIGHT_LOWER_LEG, // GOHT_BODYPART_FRONT_RIGHT_HOOF + GOHT_BODYPART_THORAX, // GOHT_BODYPART_FRONT_LEFT_UPPER_LEG + GOHT_BODYPART_FRONT_LEFT_UPPER_LEG, // GOHT_BODYPART_FRONT_LEFT_LOWER_LEG + GOHT_BODYPART_FRONT_LEFT_LOWER_LEG, // GOHT_BODYPART_FRONT_LEFT_HOOF + GOHT_BODYPART_PELVIS, // GOHT_BODYPART_BACK_RIGHT_THIGH + GOHT_BODYPART_BACK_RIGHT_THIGH, // GOHT_BODYPART_BACK_RIGHT_SHIN + GOHT_BODYPART_BACK_RIGHT_SHIN, // GOHT_BODYPART_BACK_RIGHT_HOOF + GOHT_BODYPART_PELVIS, // GOHT_BODYPART_BACK_LEFT_THIGH + GOHT_BODYPART_BACK_LEFT_THIGH, // GOHT_BODYPART_BACK_LEFT_SHIN + GOHT_BODYPART_BACK_LEFT_SHIN // GOHT_BODYPART_BACK_LEFT_HOOF +}; + +static u8 sShadowSizes[GOHT_BODYPART_MAX] = { + GOHT_SHADOW_SIZE_LARGE, // GOHT_BODYPART_PELVIS + GOHT_SHADOW_SIZE_EXTRA_LARGE, // GOHT_BODYPART_THORAX + GOHT_SHADOW_SIZE_LARGE, // GOHT_BODYPART_HEAD + GOHT_SHADOW_SIZE_MEDIUM, // GOHT_BODYPART_FRONT_RIGHT_UPPER_LEG + GOHT_SHADOW_SIZE_SMALL, // GOHT_BODYPART_FRONT_RIGHT_LOWER_LEG + GOHT_SHADOW_SIZE_SMALL, // GOHT_BODYPART_FRONT_RIGHT_HOOF + GOHT_SHADOW_SIZE_MEDIUM, // GOHT_BODYPART_FRONT_LEFT_UPPER_LEG + GOHT_SHADOW_SIZE_SMALL, // GOHT_BODYPART_FRONT_LEFT_LOWER_LEG + GOHT_SHADOW_SIZE_SMALL, // GOHT_BODYPART_FRONT_LEFT_HOOF + GOHT_SHADOW_SIZE_MEDIUM, // GOHT_BODYPART_BACK_RIGHT_THIGH + GOHT_SHADOW_SIZE_SMALL, // GOHT_BODYPART_BACK_RIGHT_SHIN + GOHT_SHADOW_SIZE_SMALL, // GOHT_BODYPART_BACK_RIGHT_HOOF + GOHT_SHADOW_SIZE_MEDIUM, // GOHT_BODYPART_BACK_LEFT_THIGH + GOHT_SHADOW_SIZE_SMALL, // GOHT_BODYPART_BACK_LEFT_SHIN + GOHT_SHADOW_SIZE_SMALL // GOHT_BODYPART_BACK_LEFT_HOOF +}; + +void BossHakugin_FillShadowTex(BossHakugin* this, u8* tex, f32 weight) { + s32 index; + s32 i; + s32 baseX; + s32 baseY; + s32 addY; + s32 x; + s32 y = BODYPART_NONE; + Vec3f lerp; + Vec3f* parentBodyPartPos; + Vec3f* bodyPartPos; + Vec3f pos; + Vec3f startVec; + + for (i = 0; i < GOHT_BODYPART_MAX; i++) { + // TODO: match with a continue + if ((weight == 0.0f) || (y = sParentShadowBodyParts[i]) > BODYPART_NONE) { + bodyPartPos = &this->bodyPartsPos[i]; + if (weight > 0.0f) { + parentBodyPartPos = &this->bodyPartsPos[y]; + + VEC3F_LERPIMPDST(&lerp, bodyPartPos, parentBodyPartPos, weight); + + pos.x = lerp.x - this->actor.world.pos.x; + pos.y = lerp.y - this->actor.world.pos.y + 76.0f + 30.0f + 30.0f + 600.0f; + pos.z = lerp.z - this->actor.world.pos.z; + } else { + pos.x = bodyPartPos->x - this->actor.world.pos.x; + pos.y = bodyPartPos->y - this->actor.world.pos.y + 76.0f + 30.0f + 30.0f + 600.0f; + pos.z = bodyPartPos->z - this->actor.world.pos.z; + } + + Matrix_MultVec3f(&pos, &startVec); + + startVec.x *= 0.07f; + startVec.y *= 0.07f; + + baseX = (u16)(s32)(startVec.x + 32.0f); + baseY = (u16)((s32)startVec.y * 64); + + if (sShadowSizes[i] == GOHT_SHADOW_SIZE_EXTRA_LARGE) { + for (y = 0, addY = -0x180; y < ARRAY_COUNT(sShadowExtraLargeMap); y++, addY += 0x40) { + for (x = -sShadowExtraLargeMap[y]; x < sShadowExtraLargeMap[y]; x++) { + index = baseX + x + baseY + addY; + if ((index >= 0) && (index < GOHT_SHADOW_TEX_SIZE)) { + tex[index] = 255; + } + } + } + } else if (sShadowSizes[i] == GOHT_SHADOW_SIZE_LARGE) { + for (y = 0, addY = -0x100; y < ARRAY_COUNT(sShadowLargeMap); y++, addY += 0x40) { + for (x = -sShadowLargeMap[y]; x < sShadowLargeMap[y]; x++) { + index = baseX + x + baseY + addY; + if ((index >= 0) && (index < GOHT_SHADOW_TEX_SIZE)) { + tex[index] = 255; + } + } + } + } else if (sShadowSizes[i] == GOHT_SHADOW_SIZE_MEDIUM) { + for (y = 0, addY = -0xC0; y < ARRAY_COUNT(sShadowMediumMap); y++, addY += 0x40) { + for (x = -sShadowMediumMap[y]; x < sShadowMediumMap[y]; x++) { + index = baseX + x + baseY + addY; + if ((index >= 0) && (index < GOHT_SHADOW_TEX_SIZE)) { + tex[index] = 255; + } + } + } + } else { + for (y = 0, addY = -0x80; y < ARRAY_COUNT(sShadowSmallMap); y++, addY += 0x40) { + for (x = -sShadowSmallMap[y]; x < sShadowSmallMap[y]; x++) { + index = baseX + x + baseY + addY; + if ((index >= 0) && (index < GOHT_SHADOW_TEX_SIZE)) { + tex[index] = 255; + } + } + } + } + } + } +} + +void BossHakugin_GenShadowTex(u8* tex, BossHakugin* this) { + s32* iter = (s32*)tex; + s16 i; + + for (i = 0; i < (s32)(GOHT_SHADOW_TEX_SIZE / sizeof(s32)); i++, iter++) { + *iter = 0; + } + + Matrix_RotateXFNew(1.0f); + + for (i = 0; i <= 5; i++) { + BossHakugin_FillShadowTex(this, tex, i / 5.0f); + } +} + +/** + * Draws Goht's dynamic shadow underneath it. + */ +void BossHakugin_DrawShadowTex(u8* tex, BossHakugin* this, PlayState* play) { + s32 pad[2]; + f32 alpha; + GraphicsContext* gfxCtx = play->state.gfxCtx; + MtxF mtx; + + OPEN_DISPS(gfxCtx); + + Gfx_SetupDL25_Opa(play->state.gfxCtx); + + alpha = (400.0f - this->actor.world.pos.y) * (1.0f / 400.0f); + alpha = CLAMP_MIN(alpha, 0.0f); + alpha = CLAMP_MAX(alpha, 1.0f); + + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 0, 0, (s8)(alpha * 80.0f)); + gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 0); + + if (this->actor.floorPoly != NULL) { + func_800C0094(this->actor.floorPoly, this->actor.world.pos.x, this->actor.floorHeight, this->actor.world.pos.z, + &mtx); + Matrix_Put(&mtx); + } + + Matrix_Scale(4.25f, 1.0f, 4.25f, MTXMODE_APPLY); + gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_OPA_DISP++, gShadowMaterialDL); + gDPLoadTextureBlock(POLY_OPA_DISP++, tex, G_IM_FMT_I, G_IM_SIZ_8b, GOHT_SHADOW_TEX_WIDTH, GOHT_SHADOW_TEX_HEIGHT, 0, + G_TX_NOMIRROR | G_TX_CLAMP, G_TX_NOMIRROR | G_TX_CLAMP, 6, 6, G_TX_NOLOD, G_TX_NOLOD); + gSPDisplayList(POLY_OPA_DISP++, gShadowModelDL); + + CLOSE_DISPS(gfxCtx); +} + +/** + * Spawns the rocks that fall down and crush Goht in its death cutscene. Each rock is placed in the air with a falling + * speed of zero; `BossHakugin_DeathCutsceneCrushedByRocks` is responsible for setting the falling speed to a non-zero + * value when it wants a rock to start falling. + */ +void BossHakugin_SpawnCrushingRocks(BossHakugin* this) { + s32 i; + Vec3f pos; + s32 num = 15; + GohtCrushingRock* crushingRock = this->crushingRocks; + s32 j; + s32 rockCount; + f32 sinY = Math_SinS(this->actor.shape.rot.y) * 65.0f; + f32 cosY = Math_CosS(this->actor.shape.rot.y) * 65.0f; + f32 sinZ = Math_SinS(this->actor.shape.rot.z) * 65.0f; + f32 scale; + f32 offsetX1; + f32 offsetZ1; + f32 offsetX2; + f32 offsetZ2; + s32 pad; + + for (i = 4; i > 0; i--, num -= 4) { + offsetX2 = (30.0f / 13.0f) * sinY; + offsetZ2 = (30.0f / 13.0f) * cosY; + + pos.x = this->actor.world.pos.x - (i * sinY) + offsetX2; + pos.y = this->actor.world.pos.y + (85.0f * (4 - i)) + 20.0f; + pos.z = this->actor.world.pos.z - (i * cosY) + offsetZ2; + + crushingRock->scale = ((i * 4.5f) + 22.0f) * 0.001f; + Math_Vec3f_Copy(&crushingRock->pos, &pos); + + rockCount = num >> 1; + if (rockCount == 0) { + break; + } + + crushingRock++; + + scale = i / (f32)rockCount; + offsetX1 = (cosY + sinY) * scale; + offsetZ1 = (cosY - sinY) * scale; + + for (j = 0; j < rockCount; j++, crushingRock++) { + crushingRock->pos.x = pos.x + offsetX1 * (rockCount - j); + crushingRock->pos.y = pos.y + i * sinZ; + crushingRock->pos.z = pos.z + offsetZ1 * (rockCount - j); + crushingRock->scale = ((i * 4.5f) + 22.0f) * 0.001f; + } + + offsetX2 = (sinY - cosY) * scale; + offsetZ2 = (cosY + sinY) * scale; + + for (j = 0; j < rockCount; j++, crushingRock++) { + crushingRock->pos.x = pos.x + offsetX2 * (rockCount - j); + crushingRock->pos.y = pos.y - i * sinZ; + crushingRock->pos.z = pos.z + offsetZ2 * (rockCount - j); + crushingRock->scale = ((i * 4.5f) + 22.0f) * 0.001f; + } + } + + for (i = 0; i < GOHT_CRUSHING_ROCK_COUNT; i++) { + crushingRock = &this->crushingRocks[i]; + + crushingRock->scale += Rand_ZeroFloat(5.0f) * 0.001f; + crushingRock->rot.x = (s32)Rand_Next() >> 0x10; + crushingRock->rot.y = (s32)Rand_Next() >> 0x10; + crushingRock->rot.z = (s32)Rand_Next() >> 0x10; + crushingRock->targetPosY = crushingRock->pos.y; + crushingRock->fallingSpeed = 0.0f; + crushingRock->pos.y += 850.0f; + } +} + +/** + * This function uses Goht's body parts colliders to set the collision info for the rocks that crushed Goht. Since Goht + * is always buried under rocks when this function is called, using its body parts colliders in this manner is fine; + * they aren't currently being used for their original purpose or for anything else. + */ +void BossHakugin_UpdateCrushingRocksCollision(BossHakugin* this) { + s32 i; + GohtCrushingRock* crushingRock; + ColliderJntSphElement* element; + + for (i = 0; i < GOHT_CRUSHING_ROCK_COUNT / 2; i++) { + crushingRock = &this->crushingRocks[i << 1]; + element = &this->bodyCollider.elements[i]; + + element->dim.worldSphere.center.x = crushingRock->pos.x; + element->dim.worldSphere.center.y = crushingRock->pos.y; + element->dim.worldSphere.center.z = crushingRock->pos.z; + element->dim.worldSphere.radius = crushingRock->scale * 3000.0f; + element->info.bumper.dmgFlags = 0xF3CFBBFF; + element->info.bumperFlags &= ~BUMP_NO_HITMARK; + element->info.elemType = ELEMTYPE_UNK0; + } + + // This for-loop will update the collider for GOHT_COLLIDER_BODYPART_LEFT_HORN. + for (; i < ARRAY_COUNT(this->bodyColliderElements); i++) { + this->bodyCollider.elements[i].info.bumperFlags &= ~BUMP_ON; + this->bodyCollider.elements[i].info.ocElemFlags &= ~OCELEM_ON; + } + + this->bodyCollider.base.colType = COLTYPE_HARD; +} + +/** + * Updates the rocks that crushed Goht after the boss fight is over. The rocks in this state don't actually move; the + * only thing this function does is update the collision for the rocks. + */ +void BossHakugin_UpdateStationaryCrushingRocks(Actor* thisx, PlayState* play2) { + PlayState* play = play2; + BossHakugin* this = THIS; + + BossHakugin_UpdateCrushingRocksCollision(this); + CollisionCheck_SetAC(play, &play->colChkCtx, &this->bodyCollider.base); + CollisionCheck_SetOC(play, &play->colChkCtx, &this->bodyCollider.base); +} + +/** + * Draws the rocks that crush Goht, both when they are falling down and after they become stationary. + */ +void BossHakugin_DrawCrushingRocks(Actor* thisx, PlayState* play) { + BossHakugin* this = THIS; + s32 i; + GohtCrushingRock* crushingRock; + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL25_Opa(play->state.gfxCtx); + gSPDisplayList(POLY_OPA_DISP++, gGohtRockMaterialDL); + + for (i = 0; i < GOHT_CRUSHING_ROCK_COUNT; i++) { + crushingRock = &this->crushingRocks[i]; + + Matrix_SetTranslateRotateYXZ(crushingRock->pos.x, crushingRock->pos.y, crushingRock->pos.z, &crushingRock->rot); + Matrix_Scale(crushingRock->scale, crushingRock->scale, crushingRock->scale, MTXMODE_APPLY); + + gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(POLY_OPA_DISP++, gGohtRockModelDL); + } + + CLOSE_DISPS(play->state.gfxCtx); +} diff --git a/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.h b/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.h index 10a01989a3..cbb96bb359 100644 --- a/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.h +++ b/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.h @@ -2,38 +2,177 @@ #define Z_BOSS_HAKUGIN_H #include "global.h" +#include "objects/object_boss_hakugin/object_boss_hakugin.h" +#include "overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.h" struct BossHakugin; +#define GOHT_SHADOW_TEX_WIDTH 64 +#define GOHT_SHADOW_TEX_HEIGHT 64 +#define GOHT_SHADOW_TEX_SIZE ((s32)sizeof(u8[GOHT_SHADOW_TEX_HEIGHT][GOHT_SHADOW_TEX_WIDTH])) + typedef void (*BossHakuginActionFunc)(struct BossHakugin*, PlayState*); -typedef struct BossHakuginEffect { - /* 0x00 */ Vec3f unk_0; - /* 0x0C */ Vec3f unk_C; - /* 0x18 */ s16 unk_18; - /* 0x1A */ s16 unk_1A; - /* 0x1C */ Vec3s unk_1C; - /* 0x24 */ f32 unk_24; -} BossHakuginEffect; // size = 0x28 -#define BOSS_HAKUGIN_EFFECT_COUNT 180 +typedef enum GohtRockEffectType { + /* 0 */ GOHT_ROCK_EFFECT_TYPE_BOULDER, + /* 1 */ GOHT_ROCK_EFFECT_TYPE_STALACTITE +} GohtRockEffectType; -typedef struct BossHakuginFhgFlashUnkStruct { - /* 0x00 */ Vec3f unk_00; - /* 0x0C */ char unk_0C[0x8]; -} BossHakuginFhgFlashUnkStruct; // size = 0x14 +typedef struct GohtRockEffect { + /* 0x00 */ Vec3f pos; + /* 0x0C */ Vec3f velocity; + /* 0x18 */ s16 timer; + /* 0x1A */ s16 type; // See GohtRockEffectType + /* 0x1C */ Vec3s rot; + /* 0x24 */ f32 scale; +} GohtRockEffect; // size = 0x28 + +typedef struct GohtCrushingRock { + /* 0x00 */ Vec3f pos; + /* 0x0C */ f32 targetPosY; + /* 0x10 */ f32 fallingSpeed; + /* 0x14 */ UNK_TYPE1 pad_14[0x8]; + /* 0x1C */ Vec3s rot; + /* 0x24 */ f32 scale; +} GohtCrushingRock; // size = 0x28 + +#define GOHT_ROCK_EFFECT_COUNT 180 +#define GOHT_CRUSHING_ROCK_COUNT 36 +#define GOHT_ELECTRIC_BALL_COUNT_MAX 10 + +typedef struct GohtLightningSegment { + /* 0x00 */ Vec3f pos; + /* 0x0C */ s16 alpha; + /* 0x0E */ Vec3s rot; + /* 0x14 */ ColliderTris collider; + /* 0x34 */ ColliderTrisElement colliderElements[1]; +} GohtLightningSegment; // size = 0x90 + +#define GOHT_LIGHTNING_SEGMENT_COUNT 20 + +typedef struct GohtMalfunctionEffect { + /* 0x00 */ Vec3f pos; + /* 0x0C */ f32 scaleXY; + /* 0x10 */ s16 alpha; + /* 0x12 */ s16 timer; +} GohtMalfunctionEffect; // size = 0x14 + +#define GOHT_MALFUNCTION_NUM_TYPES (FHGFLASH_SHOCK_GOHT_MAX - FHGFLASH_SHOCK_GOHT_BACK_LEFT_THIGH) +#define GOHT_MALFUNCTION_EFFECTS_PER_TYPE 15 + +typedef enum GohtBodyPart { + /* 0 */ GOHT_BODYPART_PELVIS, + /* 1 */ GOHT_BODYPART_THORAX, + /* 2 */ GOHT_BODYPART_HEAD, + /* 3 */ GOHT_BODYPART_FRONT_RIGHT_UPPER_LEG, + /* 4 */ GOHT_BODYPART_FRONT_RIGHT_LOWER_LEG, + /* 5 */ GOHT_BODYPART_FRONT_RIGHT_HOOF, + /* 6 */ GOHT_BODYPART_FRONT_LEFT_UPPER_LEG, + /* 7 */ GOHT_BODYPART_FRONT_LEFT_LOWER_LEG, + /* 8 */ GOHT_BODYPART_FRONT_LEFT_HOOF, + /* 9 */ GOHT_BODYPART_BACK_RIGHT_THIGH, + /* 10 */ GOHT_BODYPART_BACK_RIGHT_SHIN, + /* 11 */ GOHT_BODYPART_BACK_RIGHT_HOOF, + /* 12 */ GOHT_BODYPART_BACK_LEFT_THIGH, + /* 13 */ GOHT_BODYPART_BACK_LEFT_SHIN, + /* 14 */ GOHT_BODYPART_BACK_LEFT_HOOF, + /* 15 */ GOHT_BODYPART_MAX +} GohtBodyPart; + +typedef enum GohtColliderBodyPart { + /* 0 */ GOHT_COLLIDER_BODYPART_HEAD, + /* 1 */ GOHT_COLLIDER_BODYPART_THORAX, + /* 2 */ GOHT_COLLIDER_BODYPART_PELVIS, + /* 3 */ GOHT_COLLIDER_BODYPART_FRONT_RIGHT_UPPER_LEG, + /* 4 */ GOHT_COLLIDER_BODYPART_FRONT_RIGHT_LOWER_LEG, + /* 5 */ GOHT_COLLIDER_BODYPART_FRONT_RIGHT_HOOF, + /* 6 */ GOHT_COLLIDER_BODYPART_FRONT_LEFT_UPPER_LEG, + /* 7 */ GOHT_COLLIDER_BODYPART_FRONT_LEFT_LOWER_LEG, + /* 8 */ GOHT_COLLIDER_BODYPART_FRONT_LEFT_HOOF, + /* 9 */ GOHT_COLLIDER_BODYPART_BACK_RIGHT_THIGH, + /* 10 */ GOHT_COLLIDER_BODYPART_BACK_RIGHT_SHIN, + /* 11 */ GOHT_COLLIDER_BODYPART_BACK_RIGHT_PASTERN, + /* 12 */ GOHT_COLLIDER_BODYPART_BACK_RIGHT_HOOF, + /* 13 */ GOHT_COLLIDER_BODYPART_BACK_LEFT_THIGH, + /* 14 */ GOHT_COLLIDER_BODYPART_BACK_LEFT_SHIN, + /* 15 */ GOHT_COLLIDER_BODYPART_BACK_LEFT_PASTERN, + /* 16 */ GOHT_COLLIDER_BODYPART_BACK_LEFT_HOOF, + /* 17 */ GOHT_COLLIDER_BODYPART_RIGHT_HORN, + /* 18 */ GOHT_COLLIDER_BODYPART_LEFT_HORN, + /* 19 */ GOHT_COLLIDER_BODYPART_MAX +} GohtColliderBodyPart; typedef struct BossHakugin { /* 0x0000 */ Actor actor; - /* 0x0144 */ char unk_0144[0x44]; + /* 0x0144 */ SkelAnime skelAnime; /* 0x0188 */ BossHakuginActionFunc actionFunc; - /* 0x018C */ char unk_018C[0x5]; - /* 0x0191 */ u8 unk_191; - /* 0x018C */ char unk_0192[0x866]; - /* 0x09F8 */ BossHakuginEffect unk_9F8[BOSS_HAKUGIN_EFFECT_COUNT]; - /* 0x2618 */ char unk_2618[0x8E8]; - /* 0x2F00 */ BossHakuginFhgFlashUnkStruct unk_2F00[7][15]; - /* 0x3734 */ char unk_3734[0xDC]; + /* 0x018C */ u8 lightningHitSomething; + /* 0x018D */ u8 preventBoulderSpawnCount; + /* 0x018E */ u8 preventStalactiteSpawnCount; + /* 0x018F */ u8 preventBombSpawnCount; + /* 0x0190 */ u8 blockMalfunctionEffects; + /* 0x0191 */ u8 malfunctionEffectIndex; + /* 0x0192 */ u8 electricBallState; // See `GohtElectricBallState` + /* 0x0193 */ u8 iceAlpha; + /* 0x0194 */ u8 hasAppliedGoronSpikeBoost; + /* 0x0195 */ u8 finishedFallingDown; + /* 0x0196 */ u8 drawDmgEffType; + /* 0x0198 */ s16 lightOrbRotZ; + /* 0x019A */ s16 damagedSpeedUpCounter; + /* 0x019C */ union { + s16 nextCutsceneType; + s16 timer; + }; + /* 0x019E */ s16 targetRotY; + /* 0x01A0 */ s16 baseRotY; + /* 0x01A2 */ s16 runOffsetRot; + /* 0x01A4 */ s16 disableBodyCollidersTimer; + /* 0x01A6 */ s16 frontHalfRotZ; + /* 0x01A8 */ s16 chargeUpTimer; + /* 0x01AA */ s16 electricBallCount; + /* 0x01AC */ s16 subCamId; + /* 0x01AE */ s16 deathCutsceneRandomHeadRot; + /* 0x01B0 */ u32 limbDrawFlags; + /* 0x01B4 */ s32 direction; // See `GohtDirection` + /* 0x01B8 */ f32 electricBallSpeed; + /* 0x01BC */ f32 distToRightWall; + /* 0x01C0 */ f32 distToLeftWall; + /* 0x01C4 */ f32 targetSpeed; + /* 0x01C8 */ f32 chargingLightOrbScale; + /* 0x01CC */ f32 chargingLightningTranslateZ; + /* 0x01D0 */ f32 chargingLightningScale; + /* 0x01D4 */ f32 iceScaleY; + /* 0x01D8 */ UNK_TYPE1 unk_01D8[0x4]; // probably unused float + /* 0x01DC */ f32 drawDmgEffScale; + /* 0x01E0 */ f32 drawDmgEffFrozenSteamScale; + /* 0x01E4 */ f32 drawDmgEffAlpha; + /* 0x01E8 */ Vec3s jointTable[GOHT_LIMB_MAX]; + /* 0x02AE */ Vec3s morphTable[GOHT_LIMB_MAX]; + /* 0x0374 */ Vec3s headRot; + /* 0x037A */ Vec3s subCamRot; + /* 0x0380 */ Vec3f chargingLightningPos; + /* 0x038C */ Vec3f subCamEye; + /* 0x0398 */ Vec3f bodyPartsPos[GOHT_BODYPART_MAX]; + /* 0x044C */ Vec3f transformedPlayerPos; + /* 0x0458 */ Vec3f sfxPos; + /* 0x0464 */ Vec3f electricBallSfxPos; + /* 0x0470 */ LightNode* lightNode; + /* 0x0474 */ LightInfo lightInfo; + /* 0x0484 */ ColliderJntSph bodyCollider; + /* 0x04A4 */ ColliderJntSphElement bodyColliderElements[GOHT_COLLIDER_BODYPART_MAX]; + /* 0x0964 */ ColliderCylinder iceCollider; + /* 0x09B0 */ Actor* boulders[8]; + /* 0x09D0 */ Actor* stalactites[10]; + /* 0x09F8 */ union { + GohtRockEffect rockEffects[GOHT_ROCK_EFFECT_COUNT]; + GohtCrushingRock crushingRocks[GOHT_CRUSHING_ROCK_COUNT]; + }; + /* 0x2618 */ GohtLightningSegment lightningSegments[GOHT_LIGHTNING_SEGMENT_COUNT]; // A chain of lightning used as an attack + /* 0x3158 */ GohtMalfunctionEffect malfunctionEffects[GOHT_MALFUNCTION_NUM_TYPES][GOHT_MALFUNCTION_EFFECTS_PER_TYPE]; // Black smoke and electric zaps on body parts as damage accumulates + /* 0x3734 */ Vec3f electricBallPos[GOHT_ELECTRIC_BALL_COUNT_MAX]; + /* 0x37AC */ Vec3f electricBallRot; + /* 0x37B8 */ ColliderSphere electricBallCollider; } BossHakugin; // size = 0x3810 #endif // Z_BOSS_HAKUGIN_H diff --git a/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.c b/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.c index de1d719ddc..b98970e2a7 100644 --- a/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.c +++ b/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.c @@ -1,7 +1,26 @@ /* * File: z_en_hakurock.c * Overlay: En_Hakurock - * Description: Goht Debris + * Description: Interactive rocks in Goht's fight + * + * This actor is responsible for most of the various rocks that the player and/or Goht can interact with. It can be + * divided into three main subtypes: + * - Boulder: These are the large rocks that Goht can kick up while it's running. These boulders fly through the air and + * damage the player when they collide with them. + * - Stalactite: These are the stalactites that fall from the ceiling during the Goht fight. As it falls through the + * air, it can be destroyed if it touches Goht. Once it lands and gets stuck in the ground, it can stop the player's + * Goron Roll if they run into it, and it can be destroyed by Goht or other falling stalactites. + * - LargeStalactite: These are the stalactites that keep the player confined to a small part of the boss room when they + * first enter it. Goht destroys these stalactites as part of its intro cutscene after it's been thawed, allowing the + * player to access the rest of the room. + * + * This actor heavily assumes that it will only be spawned by Goht's (BossHakugin) actor, so it won't act correctly if + * spawned directly within the scene or by any other actor. When this actor is visibly destroyed, it's not actually + * killed; its params are set to a specific value, and it's placed into a waiting state until BossHakugin updates its + * params again to reuse the instance. Additionally, the rock effects that appear when this actor is destroyed are + * spawned by directly writing values to the parent actor's instance without ever checking its ID or if this actor even + * has a parent; it just assumes its parent is a BossHakugin instance and tries to update its rock effects array + * accordingly. */ #include "z_en_hakurock.h" @@ -17,20 +36,27 @@ void EnHakurock_Init(Actor* thisx, PlayState* play); void EnHakurock_Destroy(Actor* thisx, PlayState* play); void EnHakurock_Update(Actor* thisx, PlayState* play); -void func_80B21BE0(BossHakugin* parent, Vec3f* arg1, s32 arg2); -void func_80B21EA4(EnHakurock* this, s32 arg1); -void func_80B21FFC(EnHakurock* this); -void func_80B22040(EnHakurock* this, PlayState* play); -void func_80B220A8(EnHakurock* this); -void func_80B221E8(EnHakurock* this, PlayState* play); -void func_80B222AC(EnHakurock* this, PlayState* play); -void func_80B2242C(EnHakurock* this, PlayState* play); -void func_80B224C0(EnHakurock* this); -void func_80B22500(EnHakurock* this, PlayState* play); -void func_80B226AC(EnHakurock* this); -void func_80B22750(EnHakurock* this, PlayState* play); -void func_80B228F4(Actor* thisx, PlayState* play); -void EnHakurock_Draw(Actor* thisx, PlayState* play); +void EnHakurock_AddRockEffect(BossHakugin* parent, Vec3f* pos, s32 type); +void EnHakurock_SpawnEffect(EnHakurock* this, s32 type); +void EnHakurock_SetupWaitForSignal(EnHakurock* this); +void EnHakurock_WaitForSignal(EnHakurock* this, PlayState* play); +void EnHakurock_Boulder_SetupMove(EnHakurock* this); +void EnHakurock_Boulder_Move(EnHakurock* this, PlayState* play); +void EnHakurock_Stalactite_SetupFall(EnHakurock* this, PlayState* play); +void EnHakurock_Stalactite_Fall(EnHakurock* this, PlayState* play); +void EnHakurock_Stalactite_SetupStuckInGround(EnHakurock* this); +void EnHakurock_Stalactite_StuckInGround(EnHakurock* this, PlayState* play); +void EnHakurock_LargeStalactite_SetupWait(EnHakurock* this); +void EnHakurock_LargeStalactite_Wait(EnHakurock* this, PlayState* play); +void EnHakurock_DrawBoulder(Actor* thisx, PlayState* play); +void EnHakurock_DrawStalactite(Actor* thisx, PlayState* play); + +typedef enum EnHakurockEffectType { + /* 0 */ EN_HAKUROCK_EFFECT_TYPE_BOULDER_DESTROYED, + /* 1 */ EN_HAKUROCK_EFFECT_TYPE_FALLING_STALACTITE_DESTROYED, + /* 2 */ EN_HAKUROCK_EFFECT_TYPE_STALACTITE_STUCK, + /* 3 */ EN_HAKUROCK_EFFECT_TYPE_STALACTITE_DESTROYED +} EnHakurockEffectType; ActorInit En_Hakurock_InitVars = { /**/ ACTOR_EN_HAKUROCK, @@ -66,22 +92,22 @@ static ColliderCylinderInit sCylinderInit = { static CollisionCheckInfoInit sColChkInfoInit = { 0, 60, 60, MASS_IMMOVABLE }; -// Stalactite - void EnHakurock_Init(Actor* thisx, PlayState* play) { EnHakurock* this = THIS; ActorShape_Init(&this->actor.shape, 0.0f, ActorShadow_DrawCircle, 52.0f); Collider_InitAndSetCylinder(play, &this->collider, &this->actor, &sCylinderInit); CollisionCheck_SetInfo(&this->actor.colChkInfo, NULL, &sColChkInfoInit); - if (this->actor.params == EN_HAKUROCK_TYPE_BOULDER) { + + if (EN_HAKUROCK_GET_TYPE(&this->actor) == EN_HAKUROCK_TYPE_BOULDER) { this->actor.gravity = -1.5f; } else { this->collider.base.ocFlags1 &= ~OC1_NO_PUSH; this->actor.terminalVelocity = -100.0f; this->actor.gravity = -7.0f; } - func_80B21FFC(this); + + EnHakurock_SetupWaitForSignal(this); } void EnHakurock_Destroy(Actor* thisx, PlayState* play) { @@ -90,102 +116,121 @@ void EnHakurock_Destroy(Actor* thisx, PlayState* play) { Collider_DestroyCylinder(play, &this->collider); } -void func_80B21BE0(BossHakugin* parent, Vec3f* arg1, s32 arg2) { +/** + * Adds a single effect of the given type to the BossHakugin's rock effect array. This is more of an "internal" function + * only intended to be called by EnHakurock_SpawnEffect, which calls this function numerous times (and, sometimes, with + * a variety of different positions) to create a more complex-looking effect. + */ +void EnHakurock_AddRockEffect(BossHakugin* parent, Vec3f* pos, s32 type) { s32 i; - for (i = 0; i < ARRAY_COUNT(parent->unk_9F8); i++) { - BossHakuginEffect* gohtEffect = &parent->unk_9F8[i]; - if (gohtEffect->unk_18 < 0) { - s16 sp2E; - s16 sp2C; - f32 sp28; + for (i = 0; i < GOHT_ROCK_EFFECT_COUNT; i++) { + GohtRockEffect* rockEffect = &parent->rockEffects[i]; - Math_Vec3f_Copy(&gohtEffect->unk_0, arg1); - sp2C = Rand_S16Offset(0x1000, 0x3000); - sp2E = Rand_Next() >> 0x10; - sp28 = Rand_ZeroFloat(5.0f) + 10.0f; - gohtEffect->unk_C.x = (sp28 * Math_CosS(sp2C)) * Math_SinS(sp2E); - gohtEffect->unk_C.y = (Math_SinS(sp2C) * sp28); - gohtEffect->unk_C.z = (sp28 * Math_CosS(sp2C)) * Math_CosS(sp2E); - if ((arg2 == 1) || (arg2 == 3)) { - gohtEffect->unk_24 = ((Rand_ZeroFloat(5.0f) + 25.0f) * 0.0012f); - gohtEffect->unk_0.x = ((Rand_ZeroFloat(2.0f) + 9.0f) * gohtEffect->unk_C.x) + arg1->x; - gohtEffect->unk_0.y = ((Rand_ZeroFloat(2.0f) + 3.0f) * gohtEffect->unk_C.y) + arg1->y; - gohtEffect->unk_0.z = ((Rand_ZeroFloat(2.0f) + 9.0f) * gohtEffect->unk_C.z) + arg1->z; - gohtEffect->unk_1A = 1; + if (rockEffect->timer < 0) { + VecGeo velocityGeo; + + Math_Vec3f_Copy(&rockEffect->pos, pos); + + velocityGeo.pitch = Rand_S16Offset(0x1000, 0x3000); + velocityGeo.yaw = Rand_Next() >> 0x10; + velocityGeo.r = Rand_ZeroFloat(5.0f) + 10.0f; + rockEffect->velocity.x = velocityGeo.r * Math_CosS(velocityGeo.pitch) * Math_SinS(velocityGeo.yaw); + rockEffect->velocity.y = velocityGeo.r * Math_SinS(velocityGeo.pitch); + rockEffect->velocity.z = velocityGeo.r * Math_CosS(velocityGeo.pitch) * Math_CosS(velocityGeo.yaw); + + if ((type == EN_HAKUROCK_EFFECT_TYPE_FALLING_STALACTITE_DESTROYED) || + (type == EN_HAKUROCK_EFFECT_TYPE_STALACTITE_DESTROYED)) { + rockEffect->scale = ((Rand_ZeroFloat(5.0f) + 25.0f) * 0.0012f); + rockEffect->pos.x = pos->x + ((Rand_ZeroFloat(2.0f) + 9.0f) * rockEffect->velocity.x); + rockEffect->pos.y = pos->y + ((Rand_ZeroFloat(2.0f) + 3.0f) * rockEffect->velocity.y); + rockEffect->pos.z = pos->z + ((Rand_ZeroFloat(2.0f) + 9.0f) * rockEffect->velocity.z); + rockEffect->type = GOHT_ROCK_EFFECT_TYPE_STALACTITE; } else { - gohtEffect->unk_24 = ((Rand_ZeroFloat(5.0f) + 18.0f) * 0.0001f); - gohtEffect->unk_0.x = ((Rand_ZeroFloat(2.0f) + 3.0f) * gohtEffect->unk_C.x) + arg1->x; - gohtEffect->unk_0.y = ((Rand_ZeroFloat(3.0f) + 1.0f) * gohtEffect->unk_C.y) + arg1->y; - gohtEffect->unk_0.z = ((Rand_ZeroFloat(2.0f) + 3.0f) * gohtEffect->unk_C.z) + arg1->z; - gohtEffect->unk_1A = 0; + rockEffect->scale = ((Rand_ZeroFloat(5.0f) + 18.0f) * 0.0001f); + rockEffect->pos.x = pos->x + ((Rand_ZeroFloat(2.0f) + 3.0f) * rockEffect->velocity.x); + rockEffect->pos.y = pos->y + ((Rand_ZeroFloat(3.0f) + 1.0f) * rockEffect->velocity.y); + rockEffect->pos.z = pos->z + ((Rand_ZeroFloat(2.0f) + 3.0f) * rockEffect->velocity.z); + rockEffect->type = GOHT_ROCK_EFFECT_TYPE_BOULDER; } - gohtEffect->unk_1C.x = (s32)Rand_Next() >> 0x10; - gohtEffect->unk_1C.y = (s32)Rand_Next() >> 0x10; - gohtEffect->unk_1C.z = (s32)Rand_Next() >> 0x10; - gohtEffect->unk_18 = 0x28; + + rockEffect->rot.x = (s32)Rand_Next() >> 0x10; + rockEffect->rot.y = (s32)Rand_Next() >> 0x10; + rockEffect->rot.z = (s32)Rand_Next() >> 0x10; + rockEffect->timer = 40; return; } } } -void func_80B21EA4(EnHakurock* this, s32 arg1) { - Vec3f sp4C; +/** + * This function spawns various effects depending on what type is supplied. It assumes that the parent is a BossHakugin + * instance and calls EnHakurock_AddRockEffect multiple times to create effects with numerous rocks. + */ +void EnHakurock_SpawnEffect(EnHakurock* this, s32 type) { + Vec3f pos; s32 i; - f32 scaleFactor; - s32 tmp; - s32 phi_s5; + s32 j; + f32 offset; + s32 count; - if (arg1 == 0) { + if (type == EN_HAKUROCK_EFFECT_TYPE_BOULDER_DESTROYED) { for (i = 0; i < 20; i++) { - func_80B21BE0((BossHakugin*)this->actor.parent, &this->actor.world.pos, arg1); + EnHakurock_AddRockEffect((BossHakugin*)this->actor.parent, &this->actor.world.pos, type); } - } else if (arg1 == 2) { + } else if (type == EN_HAKUROCK_EFFECT_TYPE_STALACTITE_STUCK) { for (i = 0; i < 10; i++) { - func_80B21BE0((BossHakugin*)this->actor.parent, &this->actor.world.pos, arg1); + EnHakurock_AddRockEffect((BossHakugin*)this->actor.parent, &this->actor.world.pos, type); } } else { - Math_Vec3f_Copy(&sp4C, &this->actor.world.pos); - scaleFactor = this->actor.scale.x * 600.0f; - if (arg1 == 1) { - sp4C.y -= scaleFactor; + Math_Vec3f_Copy(&pos, &this->actor.world.pos); + offset = this->actor.scale.x * 600.0f; + if (type == EN_HAKUROCK_EFFECT_TYPE_FALLING_STALACTITE_DESTROYED) { + pos.y -= offset; i = 0; } else { i = 1; } for (; i < 3; i++) { - tmp = (i * 5) + 5; - for (phi_s5 = 0; phi_s5 < tmp; phi_s5++) { - func_80B21BE0((BossHakugin*)this->actor.parent, &sp4C, arg1); + count = (i * 5) + 5; + for (j = 0; j < count; j++) { + EnHakurock_AddRockEffect((BossHakugin*)this->actor.parent, &pos, type); } - sp4C.y += scaleFactor; + + pos.y += offset; } } } -void func_80B21FFC(EnHakurock* this) { +void EnHakurock_SetupWaitForSignal(EnHakurock* this) { this->actor.bgCheckFlags &= ~BGCHECKFLAG_GROUND; this->collider.base.atFlags &= ~AT_HIT; this->collider.base.ocFlags1 &= ~OC1_HIT; this->actor.draw = NULL; - this->actor.params = EN_HAKUROCK_TYPE_UNK_0; - this->actionFunc = func_80B22040; + this->actor.params = EN_HAKUROCK_TYPE_NONE; + this->actionFunc = EnHakurock_WaitForSignal; } -void func_80B22040(EnHakurock* this, PlayState* play) { - if (this->actor.params == EN_HAKUROCK_TYPE_BOULDER) { - func_80B220A8(this); - } else if (this->actor.params == EN_HAKUROCK_TYPE_UNK_2) { - func_80B222AC(this, play); - } else if (this->actor.params == EN_HAKUROCK_TYPE_FENCE_PILLAR) { - func_80B226AC(this); +/** + * Waits and does nothing until its params are changed, after which it will setup the appropriate action function for + * its new type. When an instance of this actor is visibly destroyed, it is put into this state to wait for BossHakugin + * to signal that its instance should be reused. If an instance of this actor is spawned by anything other than Goht, it + * will end up stuck in this state forever once it's destroyed. + */ +void EnHakurock_WaitForSignal(EnHakurock* this, PlayState* play) { + if (EN_HAKUROCK_GET_TYPE(&this->actor) == EN_HAKUROCK_TYPE_BOULDER) { + EnHakurock_Boulder_SetupMove(this); + } else if (EN_HAKUROCK_GET_TYPE(&this->actor) == EN_HAKUROCK_TYPE_FALLING_STALACTITE) { + EnHakurock_Stalactite_SetupFall(this, play); + } else if (EN_HAKUROCK_GET_TYPE(&this->actor) == EN_HAKUROCK_TYPE_LARGE_STALACTITE) { + EnHakurock_LargeStalactite_SetupWait(this); } } -void func_80B220A8(EnHakurock* this) { +void EnHakurock_Boulder_SetupMove(EnHakurock* this) { this->actor.params = EN_HAKUROCK_TYPE_BOULDER; - this->actor.draw = func_80B228F4; + this->actor.draw = EnHakurock_DrawBoulder; this->actor.speed = Rand_ZeroFloat(3.5f) + 2.5f; this->actor.velocity.y = Rand_ZeroFloat(4.5f) + 18.0f; Actor_SetScale(&this->actor, (Rand_ZeroFloat(5.0f) + 15.0f) * 0.001f); @@ -196,32 +241,36 @@ void func_80B220A8(EnHakurock* this) { this->collider.dim.radius = (this->actor.scale.x * 2500.0f); this->collider.dim.yShift = -this->collider.dim.radius; this->collider.dim.height = this->collider.dim.radius * 2; - this->counter = 10; + this->timer = 10; Math_Vec3f_Copy(&this->actor.prevPos, &this->actor.world.pos); - this->actionFunc = func_80B221E8; + this->actionFunc = EnHakurock_Boulder_Move; } -void func_80B221E8(EnHakurock* this, PlayState* play) { - if (this->counter > 0) { - this->counter--; +/** + * Flies through the air while spinning until it hits something, then it breaks apart into a bunch of small rocks. + */ +void EnHakurock_Boulder_Move(EnHakurock* this, PlayState* play) { + if (this->timer > 0) { + this->timer--; } + this->actor.shape.rot.x += 0x700; this->actor.shape.rot.y += 0x900; this->actor.shape.rot.z += 0xB00; - if ((this->collider.base.atFlags & AT_HIT) || ((this->counter == 0) && (this->collider.base.ocFlags1 & OC1_HIT)) || + if ((this->collider.base.atFlags & AT_HIT) || ((this->timer == 0) && (this->collider.base.ocFlags1 & OC1_HIT)) || ((this->actor.bgCheckFlags & BGCHECKFLAG_GROUND) && (this->actor.velocity.y < 0.0f))) { Actor_PlaySfx(&this->actor, NA_SE_EV_ROCK_BROKEN); - func_80B21EA4(this, 0); - func_80B21FFC(this); + EnHakurock_SpawnEffect(this, EN_HAKUROCK_EFFECT_TYPE_BOULDER_DESTROYED); + EnHakurock_SetupWaitForSignal(this); } } -void func_80B222AC(EnHakurock* this, PlayState* play) { +void EnHakurock_Stalactite_SetupFall(EnHakurock* this, PlayState* play) { Player* player = GET_PLAYER(play); s16 angle; - this->actor.draw = EnHakurock_Draw; + this->actor.draw = EnHakurock_DrawStalactite; angle = ((s32)Rand_Next() >> 0x13) + player->actor.shape.rot.y; this->actor.shape.rot.y = (s32)Rand_Next() >> 0x10; this->actor.world.pos.x = (Math_SinS(angle) * 600.0f) + player->actor.world.pos.x; @@ -236,46 +285,58 @@ void func_80B222AC(EnHakurock* this, PlayState* play) { this->collider.dim.yShift = (this->actor.scale.y * -750.0f); this->collider.dim.height = this->collider.dim.yShift * -2; Math_Vec3f_Copy(&this->actor.prevPos, &this->actor.world.pos); - this->actor.params = EN_HAKUROCK_TYPE_UNK_2; - this->actionFunc = func_80B2242C; + this->actor.params = EN_HAKUROCK_TYPE_FALLING_STALACTITE; + this->actionFunc = EnHakurock_Stalactite_Fall; } -void func_80B2242C(EnHakurock* this, PlayState* play) { +/** + * Falls straight down until it hits either Goht or the ground. If it hits Goht, it will break apart into a bunch of + * stalactite-shaped fragments. If it hits the ground, it will kick up some rocks and start transitioning to being stuck + * in the ground. + */ +void EnHakurock_Stalactite_Fall(EnHakurock* this, PlayState* play) { if ((this->collider.base.ocFlags1 & OC1_HIT) && (this->collider.base.oc == this->actor.parent)) { - func_80B21EA4(this, 1); - func_80B21FFC(this); + EnHakurock_SpawnEffect(this, EN_HAKUROCK_EFFECT_TYPE_FALLING_STALACTITE_DESTROYED); + EnHakurock_SetupWaitForSignal(this); } else if ((this->actor.bgCheckFlags & BGCHECKFLAG_GROUND)) { - func_80B21EA4(this, 2); + EnHakurock_SpawnEffect(this, EN_HAKUROCK_EFFECT_TYPE_STALACTITE_STUCK); Actor_PlaySfx(&this->actor, NA_SE_EV_OBJECT_STICK); - func_80B224C0(this); + EnHakurock_Stalactite_SetupStuckInGround(this); } } -void func_80B224C0(EnHakurock* this) { - this->actor.params = EN_HAKUROCK_TYPE_STALACTITE; - this->counter = 10; +void EnHakurock_Stalactite_SetupStuckInGround(EnHakurock* this) { + this->actor.params = EN_HAKUROCK_TYPE_STUCK_STALACTITE; + this->timer = 10; this->actor.shape.shadowScale = 0.0f; this->actor.world.pos.y += 4.0f; - this->actionFunc = func_80B22500; + this->actionFunc = EnHakurock_Stalactite_StuckInGround; } -void func_80B22500(EnHakurock* this, PlayState* play) { +/** + * Shakes vertically for 10 frames to make it look like it's settling into the ground. If it's struck by Goht or a + * falling stalactite, it will break apart into a bunch of stalactite-shaped fragments. If the player runs into it while + * Goron Rolling, it will stop their roll and bounce them back. If the stalactite gets too far from the player and is + * "behind" the camera, it will disappear and allow Goht to reuse this instance for another stalactite. + */ +void EnHakurock_Stalactite_StuckInGround(EnHakurock* this, PlayState* play) { Player* player = GET_PLAYER(play); - if (this->counter > 0) { - this->counter--; - if ((this->counter & 1) == 0) { - this->actor.world.pos.y = (Math_SinF(this->counter * (M_PI / 20)) * 4.0f) + this->actor.floorHeight; + if (this->timer > 0) { + this->timer--; + if ((this->timer % 2) == 0) { + this->actor.world.pos.y = (Math_SinF(this->timer * (M_PI / 20)) * 4.0f) + this->actor.floorHeight; } else { this->actor.world.pos.y = this->actor.floorHeight; } } + if (this->collider.base.ocFlags1 & OC1_HIT) { if ((this->collider.base.oc == this->actor.parent) || ((this->collider.base.oc->id == ACTOR_EN_HAKUROCK) && - (this->collider.base.oc->params == EN_HAKUROCK_TYPE_UNK_2))) { - func_80B21EA4(this, 3); - func_80B21FFC(this); + (EN_HAKUROCK_GET_TYPE(this->collider.base.oc) == EN_HAKUROCK_TYPE_FALLING_STALACTITE))) { + EnHakurock_SpawnEffect(this, EN_HAKUROCK_EFFECT_TYPE_STALACTITE_DESTROYED); + EnHakurock_SetupWaitForSignal(this); } else if ((&player->actor == this->collider.base.oc) && (player->stateFlags3 & (PLAYER_STATE3_1000 | PLAYER_STATE3_80000)) && (player->linearVelocity > 8.0f)) { @@ -287,60 +348,66 @@ void func_80B22500(EnHakurock* this, PlayState* play) { player->actor.shape.rot.y = player->actor.world.rot.y; player->unk_B8C = 4; } - } else if ((this->actor.xzDistToPlayer > 1000.0f) && (this->actor.projectedPos.z < 0.0f)) { - func_80B21FFC(this); + EnHakurock_SetupWaitForSignal(this); } } -void func_80B226AC(EnHakurock* this) { +void EnHakurock_LargeStalactite_SetupWait(EnHakurock* this) { f32 shiftFactor; - this->actor.draw = EnHakurock_Draw; + this->actor.draw = EnHakurock_DrawStalactite; Actor_SetScale(&this->actor, 0.35f); this->actor.scale.y = 0.5f; this->collider.dim.radius = this->actor.scale.x * 270.0f; shiftFactor = -750.0f; this->collider.dim.yShift = 0.5f * shiftFactor; - this->counter = 0; + this->timer = 0; this->actor.shape.shadowScale = 0.0f; this->collider.dim.height = this->collider.dim.yShift * -2; - this->actionFunc = func_80B22750; + this->actionFunc = EnHakurock_LargeStalactite_Wait; } -void func_80B22750(EnHakurock* this, PlayState* play) { - if (this->actor.params == EN_HAKUROCK_TYPE_UNK_0) { - func_80B21EA4(this, 3); - func_80B21FFC(this); +/** + * Waits until Goht changes its type as part of its intro cutscene. Once its type changes, the stalactite will break + * apart into a bunch of stalactite-shaped fragments. + */ +void EnHakurock_LargeStalactite_Wait(EnHakurock* this, PlayState* play) { + if (EN_HAKUROCK_GET_TYPE(&this->actor) == EN_HAKUROCK_TYPE_NONE) { + EnHakurock_SpawnEffect(this, EN_HAKUROCK_EFFECT_TYPE_STALACTITE_DESTROYED); + EnHakurock_SetupWaitForSignal(this); } } void EnHakurock_Update(Actor* thisx, PlayState* play) { EnHakurock* this = THIS; - s16 rockParams; + s32 pad; this->actionFunc(this, play); - rockParams = this->actor.params; - if ((rockParams == EN_HAKUROCK_TYPE_BOULDER) || (rockParams == EN_HAKUROCK_TYPE_UNK_2)) { - Actor_MoveWithGravity(&this->actor); - Actor_UpdateBgCheckInfo(play, &this->actor, 30.0f, this->collider.dim.radius, 0.0f, + + if ((EN_HAKUROCK_GET_TYPE(thisx) == EN_HAKUROCK_TYPE_BOULDER) || + (EN_HAKUROCK_GET_TYPE(thisx) == EN_HAKUROCK_TYPE_FALLING_STALACTITE)) { + Actor_MoveWithGravity(thisx); + Actor_UpdateBgCheckInfo(play, thisx, 30.0f, this->collider.dim.radius, 0.0f, UPDBGCHECKINFO_FLAG_1 | UPDBGCHECKINFO_FLAG_4 | UPDBGCHECKINFO_FLAG_80); - if (this->actor.floorHeight == BGCHECK_Y_MIN) { - func_80B21FFC(this); + + if (thisx->floorHeight == BGCHECK_Y_MIN) { + EnHakurock_SetupWaitForSignal(this); } else { - Collider_UpdateCylinder(&this->actor, &this->collider); + Collider_UpdateCylinder(thisx, &this->collider); CollisionCheck_SetAT(play, &play->colChkCtx, &this->collider.base); CollisionCheck_SetAC(play, &play->colChkCtx, &this->collider.base); CollisionCheck_SetOC(play, &play->colChkCtx, &this->collider.base); } - } else if ((rockParams == EN_HAKUROCK_TYPE_STALACTITE) || (rockParams == EN_HAKUROCK_TYPE_FENCE_PILLAR)) { - Collider_UpdateCylinder(&this->actor, &this->collider); + } else if ((EN_HAKUROCK_GET_TYPE(thisx) == EN_HAKUROCK_TYPE_STUCK_STALACTITE) || + (EN_HAKUROCK_GET_TYPE(thisx) == EN_HAKUROCK_TYPE_LARGE_STALACTITE)) { + Collider_UpdateCylinder(thisx, &this->collider); CollisionCheck_SetAC(play, &play->colChkCtx, &this->collider.base); CollisionCheck_SetOC(play, &play->colChkCtx, &this->collider.base); } } -void func_80B228F4(Actor* thisx, PlayState* play) { +void EnHakurock_DrawBoulder(Actor* thisx, PlayState* play) { OPEN_DISPS(play->state.gfxCtx); Gfx_SetupDL25_Opa(play->state.gfxCtx); @@ -351,7 +418,7 @@ void func_80B228F4(Actor* thisx, PlayState* play) { CLOSE_DISPS(play->state.gfxCtx); } -void EnHakurock_Draw(Actor* thisx, PlayState* play) { +void EnHakurock_DrawStalactite(Actor* thisx, PlayState* play) { OPEN_DISPS(play->state.gfxCtx); Gfx_SetupDL25_Opa(play->state.gfxCtx); diff --git a/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.h b/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.h index 4934c7fc82..e58c5651bc 100644 --- a/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.h +++ b/src/overlays/actors/ovl_En_Hakurock/z_en_hakurock.h @@ -5,20 +5,22 @@ struct EnHakurock; +#define EN_HAKUROCK_GET_TYPE(thisx) ((thisx)->params) + typedef void (*EnHakurockActionFunc)(struct EnHakurock*, PlayState*); typedef enum { - /* 0x0 */ EN_HAKUROCK_TYPE_UNK_0, + /* 0x0 */ EN_HAKUROCK_TYPE_NONE, /* 0x1 */ EN_HAKUROCK_TYPE_BOULDER, - /* 0x2 */ EN_HAKUROCK_TYPE_UNK_2, - /* 0x3 */ EN_HAKUROCK_TYPE_STALACTITE, - /* 0x4 */ EN_HAKUROCK_TYPE_FENCE_PILLAR + /* 0x2 */ EN_HAKUROCK_TYPE_FALLING_STALACTITE, + /* 0x3 */ EN_HAKUROCK_TYPE_STUCK_STALACTITE, + /* 0x4 */ EN_HAKUROCK_TYPE_LARGE_STALACTITE } EnHakurockType; typedef struct EnHakurock { /* 0x000 */ Actor actor; /* 0x144 */ EnHakurockActionFunc actionFunc; - /* 0x148 */ s32 counter; + /* 0x148 */ s32 timer; /* 0x14C */ ColliderCylinder collider; } EnHakurock; // size = 0x198 diff --git a/src/overlays/actors/ovl_En_Sda/z_en_sda.c b/src/overlays/actors/ovl_En_Sda/z_en_sda.c index 56a6e2c321..94e237074b 100644 --- a/src/overlays/actors/ovl_En_Sda/z_en_sda.c +++ b/src/overlays/actors/ovl_En_Sda/z_en_sda.c @@ -123,6 +123,7 @@ void func_809469C0(Player* player, u8* shadowTexture, f32 arg2) { Vec3f sp7C; for (i = 0; i < 16; i++) { + // TODO: match with a continue if ((arg2 == 0.0f) || ((j = D_80947ACC[i]) >= 0)) { if (arg2 > 0.0f) { lerp.x = D_80947EA0[i].x + (D_80947EA0[j].x - D_80947EA0[i].x) * arg2; diff --git a/src/overlays/actors/ovl_En_Tanron2/z_en_tanron2.c b/src/overlays/actors/ovl_En_Tanron2/z_en_tanron2.c index f35f5f6cac..ceb8e675f7 100644 --- a/src/overlays/actors/ovl_En_Tanron2/z_en_tanron2.c +++ b/src/overlays/actors/ovl_En_Tanron2/z_en_tanron2.c @@ -144,7 +144,7 @@ void EnTanron2_Init(Actor* thisx, PlayState* play) { Collider_InitAndSetCylinder(play, &this->collider1, &this->actor, &sCylinderInit1); Collider_InitAndSetCylinder(play, &this->collider2, &this->actor, &sCylinderInit2); - if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_60)) { + if ((KREG(64) != 0) || CHECK_EVENTINF(EVENTINF_INTRO_CS_WATCHED_WART)) { func_80BB69C0(this); } else { func_80BB6F64(this); diff --git a/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.c b/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.c index c8f5a74c72..b4bb6479cd 100644 --- a/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.c +++ b/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.c @@ -93,21 +93,24 @@ void EffectSsFhgFlash_Update(PlayState* play, u32 index, EffectSs* this) { this->pos.x = player->bodyPartsPos[randBodyPart].x + Rand_CenteredFloat(10.0f); this->pos.y = player->bodyPartsPos[randBodyPart].y + Rand_CenteredFloat(15.0f); this->pos.z = player->bodyPartsPos[randBodyPart].z + Rand_CenteredFloat(10.0f); - } else if ((this->rParams >= FHGFLASH_SHOCK_GOHT_2) && (this->rParams <= FHGFLASH_SHOCK_GOHT_6)) { + } else if ((this->rParams >= FHGFLASH_SHOCK_GOHT_BACK_LEFT_THIGH) && + (this->rParams <= FHGFLASH_SHOCK_GOHT_FRONT_RIGHT_UPPER_LEG)) { s32 pad; - Vec3f* bodyPartPos; + Vec3f* effectPos; BossHakugin* goht = (BossHakugin*)this->actor; - s32 gohtIndex = goht->unk_191 - 3; + s32 effectIndex = goht->malfunctionEffectIndex - 3; - if (gohtIndex < 0) { - bodyPartPos = &goht->unk_2F00[this->rParams][12].unk_00; + if (effectIndex < 0) { + effectPos = &goht->malfunctionEffects[this->rParams - FHGFLASH_SHOCK_GOHT_BACK_LEFT_THIGH] + [GOHT_MALFUNCTION_EFFECTS_PER_TYPE - 3] + .pos; } else { - bodyPartPos = &goht->unk_2F00[this->rParams][gohtIndex].unk_00; + effectPos = &goht->malfunctionEffects[this->rParams - FHGFLASH_SHOCK_GOHT_BACK_LEFT_THIGH][effectIndex].pos; } - this->pos.x = Rand_CenteredFloat(70.0f) + bodyPartPos->x; - this->pos.y = Rand_CenteredFloat(70.0f) + bodyPartPos->y; - this->pos.z = Rand_CenteredFloat(70.0f) + bodyPartPos->z; + this->pos.x = Rand_CenteredFloat(70.0f) + effectPos->x; + this->pos.y = Rand_CenteredFloat(70.0f) + effectPos->y; + this->pos.z = Rand_CenteredFloat(70.0f) + effectPos->z; } if (this->life < 100) { this->rAlpha -= 50; diff --git a/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.h b/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.h index 4f8cd94fe1..24575f29a2 100644 --- a/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.h +++ b/src/overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.h @@ -23,11 +23,12 @@ typedef enum { typedef enum { /* 0 */ FHGFLASH_SHOCK_NO_ACTOR, /* 1 */ FHGFLASH_SHOCK_PLAYER, - /* 2 */ FHGFLASH_SHOCK_GOHT_2, - /* 3 */ FHGFLASH_SHOCK_GOHT_3, - /* 4 */ FHGFLASH_SHOCK_GOHT_4, - /* 5 */ FHGFLASH_SHOCK_GOHT_5, - /* 6 */ FHGFLASH_SHOCK_GOHT_6 + /* 2 */ FHGFLASH_SHOCK_GOHT_BACK_LEFT_THIGH, + /* 3 */ FHGFLASH_SHOCK_GOHT_BACK_RIGHT_PASTERN, + /* 4 */ FHGFLASH_SHOCK_GOHT_JAW_ROOT, + /* 5 */ FHGFLASH_SHOCK_GOHT_FRONT_LEFT_LOWER_LEG, + /* 6 */ FHGFLASH_SHOCK_GOHT_FRONT_RIGHT_UPPER_LEG, + /* 7 */ FHGFLASH_SHOCK_GOHT_MAX } FhgFlashLightningParam; typedef struct { diff --git a/tools/disasm/functions.txt b/tools/disasm/functions.txt index 44e58fb469..b11b5f2983 100644 --- a/tools/disasm/functions.txt +++ b/tools/disasm/functions.txt @@ -12909,83 +12909,83 @@ 0x80B04D34:("func_80B04D34",), 0x80B05290:("BossHakugin_Init",), 0x80B05690:("BossHakugin_Destroy",), - 0x80B0573C:("func_80B0573C",), - 0x80B057A4:("func_80B057A4",), - 0x80B058C0:("func_80B058C0",), - 0x80B05A64:("func_80B05A64",), - 0x80B05B04:("func_80B05B04",), - 0x80B05CBC:("func_80B05CBC",), - 0x80B05D4C:("func_80B05D4C",), - 0x80B05EE0:("func_80B05EE0",), - 0x80B0607C:("func_80B0607C",), - 0x80B06558:("func_80B06558",), - 0x80B06600:("func_80B06600",), - 0x80B0696C:("func_80B0696C",), - 0x80B06B20:("func_80B06B20",), - 0x80B06C08:("func_80B06C08",), - 0x80B06D38:("func_80B06D38",), - 0x80B06F48:("func_80B06F48",), - 0x80B0728C:("func_80B0728C",), - 0x80B07450:("func_80B07450",), - 0x80B07700:("func_80B07700",), - 0x80B0791C:("func_80B0791C",), - 0x80B07B88:("func_80B07B88",), - 0x80B07BFC:("func_80B07BFC",), - 0x80B07DA4:("func_80B07DA4",), - 0x80B07EEC:("func_80B07EEC",), - 0x80B08018:("func_80B08018",), - 0x80B08124:("func_80B08124",), - 0x80B0813C:("func_80B0813C",), - 0x80B0825C:("func_80B0825C",), - 0x80B082AC:("func_80B082AC",), - 0x80B08550:("func_80B08550",), - 0x80B0863C:("func_80B0863C",), - 0x80B08848:("func_80B08848",), - 0x80B08960:("func_80B08960",), - 0x80B08C1C:("func_80B08C1C",), - 0x80B08CB8:("func_80B08CB8",), - 0x80B09178:("func_80B09178",), - 0x80B091D8:("func_80B091D8",), - 0x80B093C0:("func_80B093C0",), - 0x80B094E0:("func_80B094E0",), - 0x80B09840:("func_80B09840",), - 0x80B098BC:("func_80B098BC",), - 0x80B09A94:("func_80B09A94",), - 0x80B09C78:("func_80B09C78",), - 0x80B09DFC:("func_80B09DFC",), - 0x80B09E20:("func_80B09E20",), - 0x80B09E84:("func_80B09E84",), - 0x80B09EDC:("func_80B09EDC",), - 0x80B09F7C:("func_80B09F7C",), - 0x80B0A2A4:("func_80B0A2A4",), - 0x80B0A5EC:("func_80B0A5EC",), - 0x80B0A638:("func_80B0A638",), - 0x80B0A87C:("func_80B0A87C",), - 0x80B0A8C4:("func_80B0A8C4",), - 0x80B0AC30:("func_80B0AC30",), - 0x80B0ADFC:("func_80B0ADFC",), - 0x80B0B238:("func_80B0B238",), - 0x80B0B34C:("func_80B0B34C",), - 0x80B0B3F4:("func_80B0B3F4",), - 0x80B0B548:("func_80B0B548",), - 0x80B0B660:("func_80B0B660",), + 0x80B0573C:("BossHakugin_Vec3fNormalize",), + 0x80B057A4:("BossHakugin_StepVector",), + 0x80B058C0:("BossHakugin_SpawnLargeStalactiteWalls",), + 0x80B05A64:("BossHakugin_RequestQuakeAndRumble",), + 0x80B05B04:("BossHakugin_SpawnIceSparkle",), + 0x80B05CBC:("BossHakugin_ApplyGoronSpikeBoost",), + 0x80B05D4C:("BossHakugin_UpdateLight",), + 0x80B05EE0:("BossHakugin_SpawnBlueWarpAndHeartContainer",), + 0x80B0607C:("BossHakugin_UpdateBaseRot",), + 0x80B06558:("BossHakugin_SetLightningSegmentColliderVertices",), + 0x80B06600:("BossHakugin_AddLightningSegments",), + 0x80B0696C:("BossHakugin_SpawnGravel",), + 0x80B06B20:("BossHakugin_SpawnBoulder",), + 0x80B06C08:("BossHakugin_SpawnStalactite",), + 0x80B06D38:("BossHakugin_SpawnBomb",), + 0x80B06F48:("BossHakugin_AddMalfunctionEffects",), + 0x80B0728C:("BossHakugin_ChargeUpAttack",), + 0x80B07450:("BossHakugin_RunUpdateCommon",), + 0x80B07700:("BossHakugin_SpawnSteam",), + 0x80B0791C:("BossHakugin_ShouldWait",), + 0x80B07B88:("BossHakugin_Thaw",), + 0x80B07BFC:("BossHakugin_UpdateDrawDmgEffect",), + 0x80B07DA4:("BossHakugin_UpdateSubCam",), + 0x80B07EEC:("BossHakugin_SetupEntranceCutscene",), + 0x80B08018:("BossHakugin_EntranceCutscene",), + 0x80B08124:("BossHakugin_SetupFrozenBeforeFight",), + 0x80B0813C:("BossHakugin_FrozenBeforeFight",), + 0x80B0825C:("BossHakugin_SetupIntroCutsceneThaw",), + 0x80B082AC:("BossHakugin_IntroCutsceneThaw",), + 0x80B08550:("BossHakugin_SetupIntroCutsceneWakeUp",), + 0x80B0863C:("BossHakugin_IntroCutsceneWakeUp",), + 0x80B08848:("BossHakugin_SetupIntroCutsceneRun",), + 0x80B08960:("BossHakugin_IntroCutsceneRun",), + 0x80B08C1C:("BossHakugin_SetupRun",), + 0x80B08CB8:("BossHakugin_Run",), + 0x80B09178:("BossHakugin_SetupCharge",), + 0x80B091D8:("BossHakugin_Charge",), + 0x80B093C0:("BossHakugin_SetupDowned",), + 0x80B094E0:("BossHakugin_Downed",), + 0x80B09840:("BossHakugin_SetupThrow",), + 0x80B098BC:("BossHakugin_Throw",), + 0x80B09A94:("BossHakugin_SetupWait",), + 0x80B09C78:("BossHakugin_Wait",), + 0x80B09DFC:("BossHakugin_SetupShootLightning",), + 0x80B09E20:("BossHakugin_ShootLightning",), + 0x80B09E84:("BossHakugin_SetupCutsceneStart",), + 0x80B09EDC:("BossHakugin_CutsceneStart",), + 0x80B09F7C:("BossHakugin_SetupDeathCutsceneRun",), + 0x80B0A2A4:("BossHakugin_DeathCutsceneRun",), + 0x80B0A5EC:("BossHakugin_SetupDeathCutsceneSwerveIntoWall",), + 0x80B0A638:("BossHakugin_DeathCutsceneSwerveIntoWall",), + 0x80B0A87C:("BossHakugin_SetupDeathCutsceneCrushedByRocks",), + 0x80B0A8C4:("BossHakugin_DeathCutsceneCrushedByRocks",), + 0x80B0AC30:("BossHakugin_CheckForBodyColliderHit",), + 0x80B0ADFC:("BossHakugin_UpdateDamage",), + 0x80B0B238:("BossHakugin_UpdateRockEffects",), + 0x80B0B34C:("BossHakugin_UpdateFallingCrushingRocks",), + 0x80B0B3F4:("BossHakugin_UpdateMalfunctionEffects",), + 0x80B0B548:("BossHakugin_UpdateLightningSegments",), + 0x80B0B660:("BossHakugin_UpdateElectricBalls",), 0x80B0BAE0:("BossHakugin_Update",), - 0x80B0C000:("func_80B0C000",), - 0x80B0C1BC:("func_80B0C1BC",), - 0x80B0C398:("func_80B0C398",), - 0x80B0C570:("func_80B0C570",), - 0x80B0C7B0:("func_80B0C7B0",), - 0x80B0CAF0:("func_80B0CAF0",), - 0x80B0CCD8:("func_80B0CCD8",), - 0x80B0CF24:("func_80B0CF24",), + 0x80B0C000:("BossHakugin_OverrideLimbDraw",), + 0x80B0C1BC:("BossHakugin_PostLimbDraw",), + 0x80B0C398:("BossHakugin_DrawRockEffects",), + 0x80B0C570:("BossHakugin_DrawMalfunctionEffects",), + 0x80B0C7B0:("BossHakugin_DrawChargingLightning",), + 0x80B0CAF0:("BossHakugin_DrawLightningSegments",), + 0x80B0CCD8:("BossHakugin_DrawElectricBalls",), + 0x80B0CF24:("BossHakugin_DrawIce",), 0x80B0D0D4:("BossHakugin_Draw",), - 0x80B0D2B8:("func_80B0D2B8",), - 0x80B0D69C:("func_80B0D69C",), - 0x80B0D750:("func_80B0D750",), - 0x80B0D9CC:("func_80B0D9CC",), - 0x80B0DFA8:("func_80B0DFA8",), - 0x80B0E548:("func_80B0E548",), - 0x80B0E5A4:("func_80B0E5A4",), + 0x80B0D2B8:("BossHakugin_FillShadowTex",), + 0x80B0D69C:("BossHakugin_GenShadowTex",), + 0x80B0D750:("BossHakugin_DrawShadowTex",), + 0x80B0D9CC:("BossHakugin_SpawnCrushingRocks",), + 0x80B0DFA8:("BossHakugin_UpdateCrushingRocksCollision",), + 0x80B0E548:("BossHakugin_UpdateStationaryCrushingRocks",), + 0x80B0E5A4:("BossHakugin_DrawCrushingRocks",), 0x80B0F5E0:("func_80B0F5E0",), 0x80B0F660:("func_80B0F660",), 0x80B0F6DC:("func_80B0F6DC",), @@ -13236,21 +13236,21 @@ 0x80B21344:("EnPp_Draw",), 0x80B21B00:("EnHakurock_Init",), 0x80B21BB4:("EnHakurock_Destroy",), - 0x80B21BE0:("func_80B21BE0",), - 0x80B21EA4:("func_80B21EA4",), - 0x80B21FFC:("func_80B21FFC",), - 0x80B22040:("func_80B22040",), - 0x80B220A8:("func_80B220A8",), - 0x80B221E8:("func_80B221E8",), - 0x80B222AC:("func_80B222AC",), - 0x80B2242C:("func_80B2242C",), - 0x80B224C0:("func_80B224C0",), - 0x80B22500:("func_80B22500",), - 0x80B226AC:("func_80B226AC",), - 0x80B22750:("func_80B22750",), + 0x80B21BE0:("EnHakurock_AddRockEffect",), + 0x80B21EA4:("EnHakurock_SpawnEffect",), + 0x80B21FFC:("EnHakurock_SetupWaitForSignal",), + 0x80B22040:("EnHakurock_WaitForSignal",), + 0x80B220A8:("EnHakurock_Boulder_SetupMove",), + 0x80B221E8:("EnHakurock_Boulder_Move",), + 0x80B222AC:("EnHakurock_Stalactite_SetupFall",), + 0x80B2242C:("EnHakurock_Stalactite_Fall",), + 0x80B224C0:("EnHakurock_Stalactite_SetupStuckInGround",), + 0x80B22500:("EnHakurock_Stalactite_StuckInGround",), + 0x80B226AC:("EnHakurock_LargeStalactite_SetupWait",), + 0x80B22750:("EnHakurock_LargeStalactite_Wait",), 0x80B2278C:("EnHakurock_Update",), - 0x80B228F4:("func_80B228F4",), - 0x80B229A4:("EnHakurock_Draw",), + 0x80B228F4:("EnHakurock_DrawBoulder",), + 0x80B229A4:("EnHakurock_DrawStalactite",), 0x80B22C00:("func_80B22C00",), 0x80B22C2C:("func_80B22C2C",), 0x80B22C80:("func_80B22C80",), diff --git a/tools/disasm/variables.txt b/tools/disasm/variables.txt index c638564171..12f669bad2 100644 --- a/tools/disasm/variables.txt +++ b/tools/disasm/variables.txt @@ -13399,48 +13399,39 @@ 0x80B0504C:("D_80B0504C","f32","",0x4), 0x80B05050:("D_80B05050","f32","",0x4), 0x80B05054:("D_80B05054","f32","",0x4), - 0x80B0E6E0:("Boss_Hakugin_InitVars","UNK_TYPE1","",0x1), - 0x80B0E700:("D_80B0E700","UNK_TYPE1","",0x1), - 0x80B0E9AC:("D_80B0E9AC","UNK_TYPE1","",0x1), - 0x80B0E9BC:("D_80B0E9BC","UNK_TYPE1","",0x1), - 0x80B0E9D4:("D_80B0E9D4","UNK_TYPE1","",0x1), - 0x80B0E9F8:("D_80B0E9F8","UNK_TYPE1","",0x1), - 0x80B0EA08:("D_80B0EA08","UNK_TYPE1","",0x1), - 0x80B0EA34:("D_80B0EA34","UNK_TYPE1","",0x1), - 0x80B0EA60:("D_80B0EA60","UNK_TYPE1","",0x1), - 0x80B0EA80:("D_80B0EA80","UNK_TYPE1","",0x1), - 0x80B0EA88:("D_80B0EA88","UNK_TYPE4","",0x4), - 0x80B0EA8C:("D_80B0EA8C","UNK_TYPE1","",0x1), - 0x80B0EA94:("D_80B0EA94","UNK_TYPE1","",0x1), - 0x80B0EA95:("D_80B0EA95","UNK_TYPE1","",0x1), - 0x80B0EA9A:("D_80B0EA9A","UNK_TYPE1","",0x1), - 0x80B0EA9C:("D_80B0EA9C","UNK_TYPE1","",0x1), - 0x80B0EAA4:("D_80B0EAA4","UNK_TYPE1","",0x1), - 0x80B0EAA5:("D_80B0EAA5","UNK_TYPE1","",0x1), - 0x80B0EAAC:("D_80B0EAAC","UNK_TYPE1","",0x1), - 0x80B0EAB0:("D_80B0EAB0","UNK_TYPE1","",0x1), - 0x80B0EAC4:("D_80B0EAC4","UNK_TYPE1","",0x1), - 0x80B0EAC8:("D_80B0EAC8","UNK_TYPE1","",0x1), - 0x80B0EACC:("D_80B0EACC","UNK_TYPE1","",0x1), - 0x80B0EAD0:("D_80B0EAD0","UNK_TYPE1","",0x1), - 0x80B0EAD4:("D_80B0EAD4","UNK_TYPE4","",0x4), - 0x80B0EAD8:("D_80B0EAD8","UNK_TYPE1","",0x1), - 0x80B0EAE8:("D_80B0EAE8","UNK_TYPE1","",0x1), - 0x80B0EAF4:("D_80B0EAF4","UNK_TYPE1","",0x1), - 0x80B0EB00:("D_80B0EB00","UNK_TYPE1","",0x1), - 0x80B0EB0C:("D_80B0EB0C","UNK_TYPE1","",0x1), - 0x80B0EB18:("D_80B0EB18","UNK_TYPE1","",0x1), - 0x80B0EB24:("D_80B0EB24","UNK_TYPE1","",0x1), - 0x80B0EB38:("D_80B0EB38","UNK_TYPE1","",0x1), - 0x80B0EB60:("D_80B0EB60","UNK_TYPE1","",0x1), - 0x80B0EB68:("D_80B0EB68","UNK_TYPE4","",0x4), - 0x80B0EB6C:("D_80B0EB6C","UNK_TYPE4","",0x4), - 0x80B0EB70:("D_80B0EB70","UNK_TYPE4","",0x4), - 0x80B0EB88:("D_80B0EB88","UNK_TYPE4","",0x4), - 0x80B0EBA4:("D_80B0EBA4","UNK_TYPE4","",0x4), - 0x80B0EBC4:("D_80B0EBC4","UNK_TYPE4","",0x4), - 0x80B0EBFC:("D_80B0EBFC","UNK_TYPE4","",0x4), - 0x80B0EC38:("D_80B0EC38","UNK_TYPE1","",0x1), + 0x80B0E6E0:("Boss_Hakugin_InitVars","ActorInit","",0x20), + 0x80B0E700:("sJntSphElementsInit","ColliderJntSphElementInit","[19]",0x2AC), + 0x80B0E9AC:("sJntSphInit","ColliderJntSphInit","",0x10), + 0x80B0E9BC:("sTrisElementsInit","ColliderTrisElementInit","[1]",0x3C), + 0x80B0E9F8:("sTrisInit","ColliderTrisInit","",0x10), + 0x80B0EA08:("sSphereInit","ColliderSphereInit","",0x2C), + 0x80B0EA34:("sCylinderInit","ColliderCylinderInit","",0x2C), + 0x80B0EA60:("sDamageTable","DamageTable","",0x20), + 0x80B0EA80:("sColChkInfoInit","CollisionCheckInfoInit","",0x8), + 0x80B0EA88:("sChinTexture","TexturePtr","",0x4), + 0x80B0EA8C:("sLimbToBodyParts","s8","[33]",0x21), + 0x80B0EAB0:("sMalfunctionTypeToLimbIndex","s32","[5]",0x14), + 0x80B0EAC4:("sSparklePrimColor","Color_RGBA8","",0x4), + 0x80B0EAC8:("sSparkleEnvColor","Color_RGBA8","",0x4), + 0x80B0EACC:("sLightColor","Color_RGBA8","",0x4), + 0x80B0EAD0:("sLightningColor","Color_RGBA8","",0x4), + 0x80B0EAD4:("sTexturesDesegmented","s32","",0x4), + 0x80B0EAD8:("sInitChain","InitChainEntry","[4]",0x10), + 0x80B0EAE8:("sSubCamAt","Vec3f","",0xC), + 0x80B0EAF4:("sSubCamAt","Vec3f","",0xC), + 0x80B0EB00:("sSubCamEye","Vec3f","",0xC), + 0x80B0EB0C:("sSubCamAt","Vec3f","",0xC), + 0x80B0EB18:("sSubCamEye","Vec3f","",0xC), + 0x80B0EB24:("sRockNums","s32","[5]",0x14), + 0x80B0EB38:("sExplosionLimbHideInfo","ExplosionLimbHideInfo","[6]",0x30), + 0x80B0EB68:("sCurrentMalfunctionType","s32","",0x4), + 0x80B0EB6C:("sCurrentLimbIndex","s32","",0x4), + 0x80B0EB70:("sShadowSmallMap","s32","[6]",0x18), + 0x80B0EB88:("sShadowMediumMap","s32","[7]",0x1C), + 0x80B0EBA4:("sShadowLargeMap","s32","[8]",0x20), + 0x80B0EBC4:("sShadowExtraLargeMap","s32","[14]",0x38), + 0x80B0EBFC:("sParentShadowBodyParts","s32","[15]",0x3C), + 0x80B0EC38:("sShadowSizes","u8","[15]",0xF), 0x80B0EC50:("D_80B0EC50","f32","",0x4), 0x80B0EC54:("D_80B0EC54","f32","",0x4), 0x80B0EC58:("D_80B0EC58","f32","",0x4), diff --git a/undefined_syms.txt b/undefined_syms.txt index 3f0ad69e75..d429208cba 100644 --- a/undefined_syms.txt +++ b/undefined_syms.txt @@ -263,23 +263,6 @@ D_06041B30 = 0x06041B30; D_06042330 = 0x06042330; D_06043330 = 0x06043330; -// ovl_Boss_Hakugin - -D_06002054 = 0x06002054; -D_0600319C = 0x0600319C; -D_06010488 = 0x06010488; -D_06010500 = 0x06010500; -D_06011100 = 0x06011100; -D_06011178 = 0x06011178; -D_06011208 = 0x06011208; -D_06011278 = 0x06011278; -D_06012ED0 = 0x06012ED0; -D_06012F40 = 0x06012F40; -D_06013158 = 0x06013158; -D_060134D0 = 0x060134D0; -D_06013828 = 0x06013828; -D_06014040 = 0x06014040; - // ovl_Dm_Tsg D_06002D30 = 0x06002D30;