From bb221b4a0fb588389a0847d38ba010c0e46f34d6 Mon Sep 17 00:00:00 2001 From: Tom Overton Date: Sun, 23 Jun 2024 18:09:22 -0700 Subject: [PATCH] EnOkuta (Octorok and its projectiles) OK and documented (#1641) * Match EnOkuta * Match data * Use the generated reloc * Delete extern stuff and format * Extract the DL * Fix lots of stuff * Copy a lot of names over from EnSyatekiOkuta * SFX enums * Lots more stuff * Constants and other stuff * Some more stuff I missed * Name more functions using OoT/SyatekiOkuta as reference * An easy function I missed * Some floats * Getter macros * Types enum * Name the cylinder inits a little clearer * Create bodyparts enum and name all data * Name all functions * Name `timer` struct var and all temps * Name the `jumpHeight` struct var * Create damage effect enum * Name the extracted DL * Name `numConsecutiveProjectiles` * Finish docs * Document XML * Clarify that this blue Octorok isn't the shooting gallery one * Respond to reviews * Stuff --------- Co-authored-by: Synray <31429825+Synray@users.noreply.github.com> Co-authored-by: Derek Hensley --- assets/xml/overlays/ovl_En_Okuta.xml | 7 + linker_scripts/undefined_syms.ld | 11 - spec | 3 +- src/overlays/actors/ovl_En_Bb/z_en_bb.c | 48 +- src/overlays/actors/ovl_En_Bb/z_en_bb.h | 8 +- .../actors/ovl_En_Bbfall/z_en_bbfall.c | 48 +- .../actors/ovl_En_Bigokuta/z_en_bigokuta.c | 2 +- src/overlays/actors/ovl_En_Okuta/z_en_okuta.c | 1165 +++++++++++++++-- src/overlays/actors/ovl_En_Okuta/z_en_okuta.h | 41 +- .../ovl_En_Syateki_Okuta/z_en_syateki_okuta.c | 15 +- tools/disasm/functions.txt | 70 +- tools/disasm/variables.txt | 35 +- 12 files changed, 1210 insertions(+), 243 deletions(-) create mode 100644 assets/xml/overlays/ovl_En_Okuta.xml diff --git a/assets/xml/overlays/ovl_En_Okuta.xml b/assets/xml/overlays/ovl_En_Okuta.xml new file mode 100644 index 0000000000..dbf1247e89 --- /dev/null +++ b/assets/xml/overlays/ovl_En_Okuta.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/linker_scripts/undefined_syms.ld b/linker_scripts/undefined_syms.ld index d92b391d18..e02eac8a1a 100644 --- a/linker_scripts/undefined_syms.ld +++ b/linker_scripts/undefined_syms.ld @@ -301,17 +301,6 @@ D_06021E34 = 0x06021E34; D_06022728 = 0x06022728; D_06022CAC = 0x06022CAC; -// ovl_En_Okuta - -D_0600044C = 0x0600044C; -D_06003250 = 0x06003250; -D_060033D0 = 0x060033D0; -D_06003958 = 0x06003958; -D_06003B24 = 0x06003B24; -D_06003EE4 = 0x06003EE4; -D_06004204 = 0x06004204; -D_0600466C = 0x0600466C; - // ovl_En_Zl4 D_06013328 = 0x06013328; diff --git a/spec b/spec index c9df4a8e1d..b4fd803de9 100644 --- a/spec +++ b/spec @@ -758,8 +758,7 @@ beginseg name "ovl_En_Okuta" compress include "$(BUILD_DIR)/src/overlays/actors/ovl_En_Okuta/z_en_okuta.o" - include "$(BUILD_DIR)/data/ovl_En_Okuta/ovl_En_Okuta.data.o" - include "$(BUILD_DIR)/data/ovl_En_Okuta/ovl_En_Okuta.reloc.o" + include "$(BUILD_DIR)/src/overlays/actors/ovl_En_Okuta/ovl_En_Okuta_reloc.o" endseg beginseg diff --git a/src/overlays/actors/ovl_En_Bb/z_en_bb.c b/src/overlays/actors/ovl_En_Bb/z_en_bb.c index 2fba5f75f2..9b86248920 100644 --- a/src/overlays/actors/ovl_En_Bb/z_en_bb.c +++ b/src/overlays/actors/ovl_En_Bb/z_en_bb.c @@ -616,32 +616,30 @@ s32 EnBb_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* po } /** - * This maps a given limb based on its limbIndex to its appropriate index - * in the bodyPartsPos/Velocity arrays. + * This maps a given limb based on its limbIndex to its appropriate index in the `bodyPartsPos/Velocity` arrays. */ static s8 sLimbToBodyParts[BUBBLE_LIMB_MAX] = { - BODYPART_NONE, // BUBBLE_LIMB_NONE - BODYPART_NONE, // BUBBLE_LIMB_ROOT - BODYPART_NONE, // BUBBLE_LIMB_CRANIUM_ROOT - BODYPART_NONE, // BUBBLE_LIMB_JAW_ROOT - BUBBLE_BODYPART_0, // BUBBLE_LIMB_JAW - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_ROOT - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WRAPPER - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WEBBING_ROOT - BUBBLE_BODYPART_1, // BUBBLE_LIMB_LEFT_WING_WEBBING - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_BONE - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_ROOT - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WRAPPER - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WEBBING_ROOT - BUBBLE_BODYPART_2, // BUBBLE_LIMB_RIGHT_WING_WEBBING - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_BONE - BUBBLE_BODYPART_3, // BUBBLE_LIMB_CRANIUM + BODYPART_NONE, // BUBBLE_LIMB_NONE + BODYPART_NONE, // BUBBLE_LIMB_ROOT + BODYPART_NONE, // BUBBLE_LIMB_CRANIUM_ROOT + BODYPART_NONE, // BUBBLE_LIMB_JAW_ROOT + BUBBLE_BODYPART_JAW, // BUBBLE_LIMB_JAW + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_ROOT + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WRAPPER + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WEBBING_ROOT + BUBBLE_BODYPART_LEFT_WING_WEBBING, // BUBBLE_LIMB_LEFT_WING_WEBBING + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_BONE + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_ROOT + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WRAPPER + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WEBBING_ROOT + BUBBLE_BODYPART_RIGHT_WING_WEBBING, // BUBBLE_LIMB_RIGHT_WING_WEBBING + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_BONE + BUBBLE_BODYPART_CRANIUM, // BUBBLE_LIMB_CRANIUM }; /** - * The last element of the bodyParts arrays is a duplicate of the cranium - * limb, which is then offset by a certain amount. There is no display list - * associated with this, so it is only used for effects. + * The last element of the `bodyParts` arrays is not tied to any particular limb and is instead used to control the + * placement of effects. The positions of these effects are offset by a certain amount from the Bubble's cranium limb. */ static Vec3f sEffectsBodyPartOffset = { 1000.0f, -700.0f, 0.0f }; @@ -652,10 +650,10 @@ void EnBb_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, if (this->bodyPartDrawStatus == BB_BODY_PART_DRAW_STATUS_ALIVE) { if (sLimbToBodyParts[limbIndex] != BODYPART_NONE) { - if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_0) { - Matrix_MultVecX(1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_0]); - } else if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_3) { - Matrix_MultVecX(-1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_3]); + if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_JAW) { + Matrix_MultVecX(1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_JAW]); + } else if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_CRANIUM) { + Matrix_MultVecX(-1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_CRANIUM]); Matrix_MultVec3f(&sEffectsBodyPartOffset, &this->bodyPartsPos[BUBBLE_BODYPART_EFFECTS]); } else { Matrix_MultZero(&this->bodyPartsPos[sLimbToBodyParts[limbIndex]]); diff --git a/src/overlays/actors/ovl_En_Bb/z_en_bb.h b/src/overlays/actors/ovl_En_Bb/z_en_bb.h index 7c94d4b373..468ca1e386 100644 --- a/src/overlays/actors/ovl_En_Bb/z_en_bb.h +++ b/src/overlays/actors/ovl_En_Bb/z_en_bb.h @@ -11,10 +11,10 @@ struct EnBb; typedef void (*EnBbActionFunc)(struct EnBb*, PlayState*); typedef enum BubbleBodyPart { - /* 0 */ BUBBLE_BODYPART_0, - /* 1 */ BUBBLE_BODYPART_1, - /* 2 */ BUBBLE_BODYPART_2, - /* 3 */ BUBBLE_BODYPART_3, + /* 0 */ BUBBLE_BODYPART_JAW, + /* 1 */ BUBBLE_BODYPART_LEFT_WING_WEBBING, + /* 2 */ BUBBLE_BODYPART_RIGHT_WING_WEBBING, + /* 3 */ BUBBLE_BODYPART_CRANIUM, /* 4 */ BUBBLE_BODYPART_EFFECTS, /* 5 */ BUBBLE_BODYPART_MAX } BubbleBodyPart; diff --git a/src/overlays/actors/ovl_En_Bbfall/z_en_bbfall.c b/src/overlays/actors/ovl_En_Bbfall/z_en_bbfall.c index a224b3744c..db7a057384 100644 --- a/src/overlays/actors/ovl_En_Bbfall/z_en_bbfall.c +++ b/src/overlays/actors/ovl_En_Bbfall/z_en_bbfall.c @@ -652,32 +652,30 @@ s32 EnBbfall_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f } /** - * This maps a given limb based on its limbIndex to its appropriate index - * in the bodyPartsPos/Velocity arrays. + * This maps a given limb based on its limbIndex to its appropriate index in the `bodyPartsPos/Velocity` arrays. */ static s8 sLimbToBodyParts[BUBBLE_LIMB_MAX] = { - BODYPART_NONE, // BUBBLE_LIMB_NONE - BODYPART_NONE, // BUBBLE_LIMB_ROOT - BODYPART_NONE, // BUBBLE_LIMB_CRANIUM_ROOT - BODYPART_NONE, // BUBBLE_LIMB_JAW_ROOT - BUBBLE_BODYPART_0, // BUBBLE_LIMB_JAW - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_ROOT - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WRAPPER - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WEBBING_ROOT - BUBBLE_BODYPART_1, // BUBBLE_LIMB_LEFT_WING_WEBBING - BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_BONE - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_ROOT - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WRAPPER - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WEBBING_ROOT - BUBBLE_BODYPART_2, // BUBBLE_LIMB_RIGHT_WING_WEBBING - BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_BONE - BUBBLE_BODYPART_3, // BUBBLE_LIMB_CRANIUM + BODYPART_NONE, // BUBBLE_LIMB_NONE + BODYPART_NONE, // BUBBLE_LIMB_ROOT + BODYPART_NONE, // BUBBLE_LIMB_CRANIUM_ROOT + BODYPART_NONE, // BUBBLE_LIMB_JAW_ROOT + BUBBLE_BODYPART_JAW, // BUBBLE_LIMB_JAW + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_ROOT + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WRAPPER + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_WEBBING_ROOT + BUBBLE_BODYPART_LEFT_WING_WEBBING, // BUBBLE_LIMB_LEFT_WING_WEBBING + BODYPART_NONE, // BUBBLE_LIMB_LEFT_WING_BONE + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_ROOT + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WRAPPER + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_WEBBING_ROOT + BUBBLE_BODYPART_RIGHT_WING_WEBBING, // BUBBLE_LIMB_RIGHT_WING_WEBBING + BODYPART_NONE, // BUBBLE_LIMB_RIGHT_WING_BONE + BUBBLE_BODYPART_CRANIUM, // BUBBLE_LIMB_CRANIUM }; /** - * The last element of the bodyParts arrays is a duplicate of the cranium - * limb, which is then offset by a certain amount. There is no display list - * associated with this, so it is only used for effects. + * The last element of the `bodyParts` arrays is not tied to any particular limb and is instead used to control the + * placement of effects. The positions of these effects are offset by a certain amount from the Bubble's cranium limb. */ static Vec3f sEffectsBodyPartOffset = { 1000.0f, -700.0f, 0.0f }; @@ -688,10 +686,10 @@ void EnBbfall_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* r if (this->bodyPartDrawStatus == BBFALL_BODY_PART_DRAW_STATUS_ALIVE) { if (sLimbToBodyParts[limbIndex] != BODYPART_NONE) { - if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_0) { - Matrix_MultVecX(1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_0]); - } else if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_3) { - Matrix_MultVecX(-1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_3]); + if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_JAW) { + Matrix_MultVecX(1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_JAW]); + } else if (sLimbToBodyParts[limbIndex] == BUBBLE_BODYPART_CRANIUM) { + Matrix_MultVecX(-1000.0f, &this->bodyPartsPos[BUBBLE_BODYPART_CRANIUM]); Matrix_MultVec3f(&sEffectsBodyPartOffset, &this->bodyPartsPos[BUBBLE_BODYPART_EFFECTS]); } else { Matrix_MultZero(&this->bodyPartsPos[sLimbToBodyParts[limbIndex]]); diff --git a/src/overlays/actors/ovl_En_Bigokuta/z_en_bigokuta.c b/src/overlays/actors/ovl_En_Bigokuta/z_en_bigokuta.c index c130d5c6ae..0152b12ecf 100644 --- a/src/overlays/actors/ovl_En_Bigokuta/z_en_bigokuta.c +++ b/src/overlays/actors/ovl_En_Bigokuta/z_en_bigokuta.c @@ -551,7 +551,7 @@ void EnBigokuta_Update(Actor* thisx, PlayState* play) { Math_StepToF(&this->drawDmgEffAlpha, 0.0f, 0.05f); this->drawDmgEffScale = (this->drawDmgEffAlpha + 1.0f) * 0.6f; this->drawDmgEffScale = CLAMP_MAX(this->drawDmgEffScale, 1.2f); - } else if (!Math_StepToF(&this->drawDmgEffFrozenSteamScale, 1.2f, 0.030000001f)) { + } else if (!Math_StepToF(&this->drawDmgEffFrozenSteamScale, 1.2f, 30.0f * 0.001f)) { Actor_PlaySfx_Flagged(&this->picto.actor, NA_SE_EV_ICE_FREEZE - SFX_FLAG); } } diff --git a/src/overlays/actors/ovl_En_Okuta/z_en_okuta.c b/src/overlays/actors/ovl_En_Okuta/z_en_okuta.c index 8c8e3ce41b..2c0c1d6e49 100644 --- a/src/overlays/actors/ovl_En_Okuta/z_en_okuta.c +++ b/src/overlays/actors/ovl_En_Okuta/z_en_okuta.c @@ -1,33 +1,48 @@ /* * File: z_en_okuta.c * Overlay: ovl_En_Okuta - * Description: Octorok + * Description: Octorok and its projectiles + * + * In addition to the standard red Octorok that appears in the final game, this actor is also responsible for an unused + * blue Octorok variant. This blue Octorok is invulnerable to every attack (it simply spins whenever the player hits it + * with something), cannot be frozen with Ice Arrows, and can follow the player underwater if the player dives below the + * surface. Note that this blue Octorok is separate from the one that is used in the Town Shooting Gallery; that Octorok + * is handled by EnSyatekiOkuta. This actor also handles the projectiles that the Octoroks shoot. */ #include "z_en_okuta.h" +#include "overlays/actors/ovl_En_Clear_Tag/z_en_clear_tag.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_UNFRIENDLY) #define THIS ((EnOkuta*)thisx) -void EnOkuta_Init(Actor* thisx, PlayState* play); +void EnOkuta_Init(Actor* thisx, PlayState* play2); void EnOkuta_Destroy(Actor* thisx, PlayState* play); -void EnOkuta_Update(Actor* thisx, PlayState* play); +void EnOkuta_Update(Actor* thisx, PlayState* play2); void EnOkuta_Draw(Actor* thisx, PlayState* play); -void func_8086E52C(EnOkuta* this, PlayState* play); -void func_8086E658(EnOkuta* this, PlayState* play); -void func_8086E7E8(EnOkuta* this, PlayState* play); -void func_8086E948(EnOkuta* this, PlayState* play); -void func_8086EC00(EnOkuta* this, PlayState* play); -void func_8086EF14(EnOkuta* this, PlayState* play); -void func_8086EFE8(EnOkuta* this, PlayState* play); -void func_8086F434(EnOkuta* this, PlayState* play); -void func_8086F4B0(EnOkuta* this, PlayState* play); -void func_8086F57C(EnOkuta* this, PlayState* play); -void func_8086F694(EnOkuta* this, PlayState* play); +void EnOkuta_SetupWaitToAppear(EnOkuta* this); +void EnOkuta_WaitToAppear(EnOkuta* this, PlayState* play); +void EnOkuta_SetupAppear(EnOkuta* this, PlayState* play); +void EnOkuta_Appear(EnOkuta* this, PlayState* play); +void EnOkuta_SetupHide(EnOkuta* this); +void EnOkuta_Hide(EnOkuta* this, PlayState* play); +void EnOkuta_SetupFloat(EnOkuta* this); +void EnOkuta_Float(EnOkuta* this, PlayState* play); +void EnOkuta_SetupShoot(EnOkuta* this, PlayState* play); +void EnOkuta_Shoot(EnOkuta* this, PlayState* play); +void EnOkuta_Damaged(EnOkuta* this, PlayState* play); +void EnOkuta_SetupDie(EnOkuta* this); +void EnOkuta_Die(EnOkuta* this, PlayState* play); +void EnOkuta_FrozenInIceBlock(EnOkuta* this, PlayState* play); +void EnOkuta_Frozen(EnOkuta* this, PlayState* play); +void EnOkuta_Spin(EnOkuta* this, PlayState* play); +void EnOkuta_Projectile_Fly(EnOkuta* this, PlayState* play); +void EnOkuta_Projectile_Update(Actor* thisx, PlayState* play); + +#include "assets/overlays/ovl_En_Okuta/ovl_En_Okuta.c" -#if 0 ActorInit En_Okuta_InitVars = { /**/ ACTOR_EN_OKUTA, /**/ ACTORCAT_ENEMY, @@ -40,153 +55,1079 @@ ActorInit En_Okuta_InitVars = { /**/ EnOkuta_Draw, }; -// static ColliderCylinderInit sCylinderInit = { -static ColliderCylinderInit D_808708A0 = { - { COLTYPE_NONE, AT_ON | AT_TYPE_ENEMY, AC_ON | AC_TYPE_PLAYER, OC1_ON | OC1_TYPE_ALL, OC2_TYPE_2, COLSHAPE_CYLINDER, }, - { ELEMTYPE_UNK4, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_HARD, BUMP_ON, OCELEM_ON, }, +static ColliderCylinderInit sProjectileCylinderInit = { + { + COLTYPE_NONE, + AT_ON | AT_TYPE_ENEMY, + AC_ON | AC_TYPE_PLAYER, + OC1_ON | OC1_TYPE_ALL, + OC2_TYPE_2, + COLSHAPE_CYLINDER, + }, + { + ELEMTYPE_UNK4, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_HARD, + BUMP_ON, + OCELEM_ON, + }, { 13, 20, 0, { 0, 0, 0 } }, }; -// static ColliderCylinderInit sCylinderInit = { -static ColliderCylinderInit D_808708CC = { - { COLTYPE_HIT0, AT_ON | AT_TYPE_ENEMY, AC_ON | AC_TYPE_PLAYER, OC1_ON | OC1_TYPE_ALL, OC2_TYPE_1, COLSHAPE_CYLINDER, }, - { ELEMTYPE_UNK1, { 0xF7CFFFFF, 0x00, 0x04 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_HARD, BUMP_ON, OCELEM_ON, }, +static ColliderCylinderInit sOctorokCylinderInit = { + { + COLTYPE_HIT0, + AT_ON | AT_TYPE_ENEMY, + AC_ON | AC_TYPE_PLAYER, + OC1_ON | OC1_TYPE_ALL, + OC2_TYPE_1, + COLSHAPE_CYLINDER, + }, + { + ELEMTYPE_UNK1, + { 0xF7CFFFFF, 0x00, 0x04 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_ON | TOUCH_SFX_HARD, + BUMP_ON, + OCELEM_ON, + }, { 20, 40, -30, { 0, 0, 0 } }, }; -// sColChkInfoInit -static CollisionCheckInfoInit D_808708F8 = { 4, 15, 60, 100 }; +static CollisionCheckInfoInit sColChkInfoInit = { 4, 15, 60, 100 }; -// static DamageTable sDamageTable = { -static DamageTable D_80870900 = { - /* Deku Nut */ DMG_ENTRY(0, 0x0), - /* Deku Stick */ DMG_ENTRY(1, 0x0), - /* Horse trample */ DMG_ENTRY(0, 0x0), - /* Explosives */ DMG_ENTRY(1, 0x0), - /* Zora boomerang */ DMG_ENTRY(1, 0x0), - /* Normal arrow */ DMG_ENTRY(1, 0x0), - /* 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(0, 0x0), - /* Fire arrow */ DMG_ENTRY(1, 0x0), - /* Ice arrow */ DMG_ENTRY(2, 0x3), - /* Light arrow */ DMG_ENTRY(2, 0x4), - /* Goron spikes */ DMG_ENTRY(1, 0x0), - /* Deku spin */ DMG_ENTRY(1, 0x0), - /* Deku bubble */ DMG_ENTRY(1, 0x0), - /* Deku launch */ DMG_ENTRY(2, 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(0, 0x0), - /* 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, 0x0), +typedef enum EnOkutaDamageEffect { + /* 0x0 */ EN_OKUTA_DMGEFF_NONE, + /* 0x3 */ EN_OKUTA_DMGEFF_FREEZE = 0x3, + /* 0x4 */ EN_OKUTA_DMGEFF_LIGHT_ORB +} EnOkutaDamageEffect; + +static DamageTable sDamageTable = { + /* Deku Nut */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Deku Stick */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Horse trample */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Explosives */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Zora boomerang */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Normal arrow */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* UNK_DMG_0x06 */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Hookshot */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Goron punch */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Sword */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Goron pound */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Fire arrow */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Ice arrow */ DMG_ENTRY(2, EN_OKUTA_DMGEFF_FREEZE), + /* Light arrow */ DMG_ENTRY(2, EN_OKUTA_DMGEFF_LIGHT_ORB), + /* Goron spikes */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Deku spin */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Deku bubble */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Deku launch */ DMG_ENTRY(2, EN_OKUTA_DMGEFF_NONE), + /* UNK_DMG_0x12 */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Zora barrier */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Normal shield */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Light ray */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Thrown object */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Zora punch */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Spin attack */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), + /* Sword beam */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Normal Roll */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* UNK_DMG_0x1B */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* UNK_DMG_0x1C */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Unblockable */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* UNK_DMG_0x1E */ DMG_ENTRY(0, EN_OKUTA_DMGEFF_NONE), + /* Powder Keg */ DMG_ENTRY(1, EN_OKUTA_DMGEFF_NONE), }; -// static InitChainEntry sInitChain[] = { -static InitChainEntry D_80870920[] = { +static InitChainEntry sInitChain[] = { ICHAIN_S8(hintId, TATL_HINT_ID_OCTOROK, ICHAIN_CONTINUE), ICHAIN_F32(targetArrowOffset, 6500, ICHAIN_STOP), }; -#endif +void EnOkuta_Init(Actor* thisx, PlayState* play2) { + PlayState* play = play2; + EnOkuta* this = THIS; + WaterBox* waterBox; + f32 waterSurface; + s32 bgId; -extern ColliderCylinderInit D_808708A0; -extern ColliderCylinderInit D_808708CC; -extern CollisionCheckInfoInit D_808708F8; -extern DamageTable D_80870900; -extern InitChainEntry D_80870920[]; + Actor_ProcessInitChain(thisx, sInitChain); + this->numConsecutiveProjectiles = EN_OKUTA_GET_NUM_CONSECUTIVE_PROJECTILES(thisx); + thisx->params &= 0xFF; -extern UNK_TYPE D_0600044C; -extern UNK_TYPE D_06003250; -extern UNK_TYPE D_06003958; -extern UNK_TYPE D_06003B24; -extern UNK_TYPE D_06003EE4; -extern UNK_TYPE D_06004204; -extern UNK_TYPE D_0600466C; + if ((EN_OKUTA_GET_TYPE(thisx) == EN_OKUTA_TYPE_RED_OCTOROK) || + (EN_OKUTA_GET_TYPE(thisx) == EN_OKUTA_TYPE_BLUE_OCTOROK)) { + SkelAnime_Init(play, &this->skelAnime, &gOctorokSkel, &gOctorokAppearAnim, this->jointTable, this->morphTable, + OCTOROK_LIMB_MAX); + Collider_InitAndSetCylinder(play, &this->collider, thisx, &sOctorokCylinderInit); + CollisionCheck_SetInfo(&thisx->colChkInfo, &sDamageTable, &sColChkInfoInit); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/EnOkuta_Init.s") + if (this->numConsecutiveProjectiles == 0xFF || this->numConsecutiveProjectiles == 0) { + this->numConsecutiveProjectiles = 1; + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/EnOkuta_Destroy.s") + thisx->floorHeight = + BgCheck_EntityRaycastFloor5(&play->colCtx, &thisx->floorPoly, &bgId, thisx, &thisx->world.pos); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E084.s") + if (!WaterBox_GetSurface1_2(play, &play->colCtx, thisx->world.pos.x, thisx->world.pos.z, &waterSurface, + &waterBox) || + waterSurface <= thisx->floorHeight) { + Actor_Kill(thisx); + } else { + thisx->home.pos.y = waterSurface; + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E0F0.s") + if (EN_OKUTA_GET_TYPE(thisx) == EN_OKUTA_TYPE_BLUE_OCTOROK) { + this->collider.base.colType = COLTYPE_HARD; + this->collider.base.acFlags |= AC_HARD; + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E168.s") + thisx->targetMode = TARGET_MODE_5; + EnOkuta_SetupWaitToAppear(this); + } else { + ActorShape_Init(&thisx->shape, 1100.0f, ActorShadow_DrawCircle, 18.0f); + thisx->flags &= ~ACTOR_FLAG_TARGETABLE; + thisx->flags |= ACTOR_FLAG_10; + Collider_InitAndSetCylinder(play, &this->collider, thisx, &sProjectileCylinderInit); + Actor_ChangeCategory(play, &play->actorCtx, thisx, ACTORCAT_PROP); + this->timer = 22; + thisx->shape.rot.y = 0; + thisx->world.rot.x = -thisx->shape.rot.x; + thisx->shape.rot.x = 0; + this->actionFunc = EnOkuta_Projectile_Fly; + thisx->update = EnOkuta_Projectile_Update; + thisx->speed = 10.0f; + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E214.s") +void EnOkuta_Destroy(Actor* thisx, PlayState* play) { + EnOkuta* this = THIS; + Collider_DestroyCylinder(play, &this->collider); +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E27C.s") +/** + * Covers the Octorok's body parts with ice and turns the Octorok red. Note that this is different from being frozen + * inside a block of ice; see `EnOkuta_FrozenInIceBlock` for how that is handled. + */ +void EnOkuta_Freeze(EnOkuta* this) { + this->drawDmgEffType = ACTOR_DRAW_DMGEFF_FROZEN_NO_SFX; + this->drawDmgEffScale = 0.6f; + this->drawDmgEffFrozenSteamScale = 9.0f * 0.1f; + this->drawDmgEffAlpha = 1.0f; + this->timer = 80; + this->collider.base.colType = COLTYPE_HIT3; + Actor_SetColorFilter(&this->actor, COLORFILTER_COLORFLAG_RED, 255, COLORFILTER_BUFFLAG_OPA, 80); +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E2C0.s") +/** + * Spawns ice shards on all of the Octorok's body parts that fly off in random directions. Note that this only occurs + * when the Octorok is frozen *without* encasing it in a block of ice. + */ +void EnOkuta_Thaw(EnOkuta* 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, EN_OKUTA_BODYPART_MAX, 2, 0.3f, 0.2f); + this->collider.base.colType = COLTYPE_HIT0; + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E378.s") +void EnOkuta_SpawnBubbles(EnOkuta* this, PlayState* play) { + s32 i; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E3B8.s") + for (i = 0; i < 10; i++) { + EffectSsBubble_Spawn(play, &this->actor.world.pos, -10.0f, 10.0f, 30.0f, 0.25f); + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E4FC.s") +/** + * Spawns the puff of smoke that appears at the Octorok's snout. + */ +void EnOkuta_SpawnSmoke(Vec3f* pos, Vec3f* velocity, s16 scaleStep, PlayState* play) { + static Color_RGBA8 sSmokePrimColor = { 255, 255, 255, 255 }; + static Color_RGBA8 sSmokeEnvColor = { 150, 150, 150, 255 }; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E52C.s") + func_800B0DE0(play, pos, velocity, &gZeroVec3f, &sSmokePrimColor, &sSmokeEnvColor, 400, scaleStep); +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E5E8.s") +/** + * Spawns the splash that appears when the Octorok appears from underwater, hides underwater, shoots a rock, or dies. + */ +void EnOkuta_SpawnSplash(EnOkuta* this, PlayState* play) { + EffectSsGSplash_Spawn(play, &this->actor.home.pos, NULL, NULL, 0, 1300); +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E658.s") +void EnOkuta_SpawnRipple(EnOkuta* this, PlayState* play) { + f32 diffY = this->actor.world.pos.y - this->actor.home.pos.y; + Vec3f pos; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E7A8.s") + if (((play->gameplayFrames % 7) == 0) && (diffY < 50.0f) && (diffY >= -20.0f)) { + pos.x = this->actor.world.pos.x; + pos.y = this->actor.home.pos.y; + pos.z = this->actor.world.pos.z; + EffectSsGRipple_Spawn(play, &pos, 250, 650, 0); + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E7E8.s") +/** + * Returns the height at which the unused blue Octorok variant should float at within the water. This allows for the + * Octorok to follow the player underwater, but it does not take collision into account. + */ +f32 EnOkuta_GetFloatHeight(EnOkuta* this) { + f32 height = this->actor.world.pos.y + this->actor.playerHeightRel + 60.0f; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E8E8.s") + // `EnOkuta_Init` sets the Octorok's home y-position to be the water's surface, so this ensures that the Octorok + // will always be either at the water's surface or below it. + if (this->actor.home.pos.y < height) { + return this->actor.home.pos.y; + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086E948.s") + return height; +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086EAE0.s") +void EnOkuta_SpawnProjectile(EnOkuta* this, PlayState* play) { + Vec3f pos; + Vec3f velocity; + f32 sin = Math_SinS(this->actor.shape.rot.y); + f32 cos = Math_CosS(this->actor.shape.rot.y); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086EC00.s") + pos.x = this->actor.world.pos.x + 25.0f * sin; + pos.y = this->actor.world.pos.y - 6.0f; + pos.z = this->actor.world.pos.z + 25.0f * cos; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086EE8C.s") + if (Actor_Spawn(&play->actorCtx, play, ACTOR_EN_OKUTA, pos.x, pos.y, pos.z, this->actor.shape.rot.x, + this->actor.shape.rot.y, this->actor.shape.rot.z, + EN_OKUTA_GET_TYPE(&this->actor) + EN_OKUTA_TYPE_PROJECTILE_BASE) != NULL) { + pos.x = this->actor.world.pos.x + (40.0f * sin); + pos.z = this->actor.world.pos.z + (40.0f * cos); + pos.y = this->actor.world.pos.y; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086EF14.s") + velocity.x = 1.5f * sin; + velocity.y = 0.0f; + velocity.z = 1.5f * cos; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086EF90.s") + EnOkuta_SpawnSmoke(&pos, &velocity, 20, play); + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086EFE8.s") + Actor_PlaySfx(&this->actor, NA_SE_EN_NUTS_THROW); +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086F2FC.s") +void EnOkuta_SetupWaitToAppear(EnOkuta* this) { + this->actor.draw = NULL; + this->actor.world.pos.y = this->actor.home.pos.y; + this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; + this->actionFunc = EnOkuta_WaitToAppear; +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086F434.s") +/** + * Waits until the player gets close, but not *too* close, then makes the Octorok appear at the water's surface. + */ +void EnOkuta_WaitToAppear(EnOkuta* this, PlayState* play) { + this->actor.world.pos.y = this->actor.home.pos.y; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086F4B0.s") + if (this->actor.xzDistToPlayer < 480.0f && this->actor.xzDistToPlayer > 200.0f) { + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) { + EnOkuta_SetupAppear(this, play); + } else if (ABS_ALT((s16)(this->actor.yawTowardsPlayer - this->actor.world.rot.y)) < 0x4000 && + play->unk_1887C == 0) { + EnOkuta_SetupAppear(this, play); + } + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086F4F4.s") +void EnOkuta_SetupAppear(EnOkuta* this, PlayState* play) { + this->actor.draw = EnOkuta_Draw; + this->actor.shape.rot.y = this->actor.yawTowardsPlayer; + this->actor.flags |= ACTOR_FLAG_TARGETABLE; + Animation_PlayOnce(&this->skelAnime, &gOctorokAppearAnim); + EnOkuta_SpawnBubbles(this, play); + this->actionFunc = EnOkuta_Appear; +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086F57C.s") +/** + * Plays the appear animation to completion while scaling the Octorok to its full size, then transitions to the idle + * floating state. If the Octorok is a red Octorok, and the player is too close to the Octorok when its appear animation + * is complete, then the Octorok will instead hide underwater. + */ +void EnOkuta_Appear(EnOkuta* this, PlayState* play) { + if (SkelAnime_Update(&this->skelAnime)) { + if ((this->actor.xzDistToPlayer < 160.0f) && (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK)) { + EnOkuta_SetupHide(this); + } else { + EnOkuta_SetupFloat(this); + } + } else { + f32 curFrame = this->skelAnime.curFrame; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086F694.s") + if (curFrame <= 4.0f) { + Actor_SetScale(&this->actor, curFrame * 0.25f * 0.01f); + } else if (Animation_OnFrame(&this->skelAnime, 5.0f)) { + Actor_SetScale(&this->actor, 0.01f); + } + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086F8FC.s") + if (Animation_OnFrame(&this->skelAnime, 2.0f)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_OCTAROCK_JUMP); + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_8086FCA4.s") + if (Animation_OnFrame(&this->skelAnime, 12.0f)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_DAIOCTA_LAND); + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/EnOkuta_Update.s") + if ((Animation_OnFrame(&this->skelAnime, 3.0f)) || (Animation_OnFrame(&this->skelAnime, 15.0f))) { + EnOkuta_SpawnSplash(this, play); + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_808700C0.s") +void EnOkuta_SetupHide(EnOkuta* this) { + Animation_PlayOnce(&this->skelAnime, &gOctorokHideAnim); + this->actionFunc = EnOkuta_Hide; +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_80870254.s") +/** + * Retreats underwater while scaling the Octorok down, then makes the Octorok wait to appear again. + */ +void EnOkuta_Hide(EnOkuta* this, PlayState* play) { + Math_ApproachF(&this->actor.world.pos.y, this->actor.home.pos.y, 0.5f, 30.0f); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_808704DC.s") + if (SkelAnime_Update(&this->skelAnime)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_COMMON_WATER_MID); + EnOkuta_SpawnBubbles(this, play); + EnOkuta_SetupWaitToAppear(this); + } else { + f32 curFrame = this->skelAnime.curFrame; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/func_808705C8.s") + if (curFrame >= 4.0f) { + Actor_SetScale(&this->actor, (6.0f - curFrame) * 0.5f * 0.01f); + } + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Okuta/EnOkuta_Draw.s") + if (Animation_OnFrame(&this->skelAnime, 2.0f)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_DAIOCTA_SINK); + } + + if (Animation_OnFrame(&this->skelAnime, 4.0f)) { + EnOkuta_SpawnSplash(this, play); + } +} + +void EnOkuta_SetupFloat(EnOkuta* this) { + Animation_PlayLoop(&this->skelAnime, &gOctorokFloatAnim); + + // `EnOkuta_Float` uses `timer` to track how many times the float animation loops before shooting at the player. + if (this->actionFunc == EnOkuta_Shoot) { + this->timer = 8; + } else { + this->timer = 0; + } + + this->actionFunc = EnOkuta_Float; +} + +/** + * Floats in place while slowly turning to face the player. If the player is too far from the Octorok, it will hide + * underwater (the red Octoroks will also do this if the player is too close). Otherwise, it will start shooting + * projectiles at the player after a short time, so long as the Octorok is roughly facing the player. + */ +void EnOkuta_Float(EnOkuta* this, PlayState* play) { + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) { + this->actor.world.pos.y = this->actor.home.pos.y; + } else { + this->actor.world.pos.y = EnOkuta_GetFloatHeight(this); + } + + SkelAnime_Update(&this->skelAnime); + + if (Animation_OnFrame(&this->skelAnime, 0.0f)) { + DECR(this->timer); + } + + if (Animation_OnFrame(&this->skelAnime, 0.5f)) { + Actor_PlaySfx(&this->actor, NA_SE_EN_COMMON_WATER_SLW); + } + + if ((this->actor.xzDistToPlayer > 560.0f) || + ((this->actor.xzDistToPlayer < 160.0f) && (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK))) { + EnOkuta_SetupHide(this); + } else { + s16 yawDiff = Math_SmoothStepToS(&this->actor.shape.rot.y, this->actor.yawTowardsPlayer, 3, 0xE38, 0x38E); + + if ((ABS_ALT(yawDiff) < 0x38E) && ((((EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK)) && + (this->timer == 0) && (this->actor.playerHeightRel < 120.0f)) || + ((EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_BLUE_OCTOROK) && + ((this->timer == 0) || (this->actor.xzDistToPlayer < 150.0f))))) { + EnOkuta_SetupShoot(this, play); + } + } +} + +void EnOkuta_SetupShoot(EnOkuta* this, PlayState* play) { + Animation_PlayOnce(&this->skelAnime, &gOctorokShootAnim); + + if (this->actionFunc != EnOkuta_Shoot) { + // `EnOkuta_Shoot` uses `timer` to track how many projectiles are remaining in the current "volley". The Octorok + // will shoot at the player repeatedly, decrementing the `timer` variable after each shot, until it reaches 0. + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) { + this->timer = this->numConsecutiveProjectiles; + } else { + this->timer = (560.0f - this->actor.xzDistToPlayer) * 0.005f + 1.0f; + } + } + + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) { + this->jumpHeight = this->actor.playerHeightRel + 20.0f; + this->jumpHeight = CLAMP_MIN(this->jumpHeight, 10.0f); + + if (this->jumpHeight > 50.0f) { + EnOkuta_SpawnSplash(this, play); + Actor_PlaySfx(&this->actor, NA_SE_EN_OCTAROCK_JUMP); + } + } + + this->actionFunc = EnOkuta_Shoot; +} + +/** + * Repeatedly fires projectiles at the player based on the value of the `numConsecutiveProjectiles` variable (for the + * red Octorok) or based on the distance between the Octorok and the player (for the blue Octorok). Once it's done + * shooting projectiles, the Octorok will go back to floating. If it's a red Octorok, then it will stop shooting and + * hide underwater if the player gets too close. + */ +void EnOkuta_Shoot(EnOkuta* this, PlayState* play) { + f32 curFrame; + + Math_ApproachS(&this->actor.shape.rot.y, this->actor.yawTowardsPlayer, 3, 0x71C); + + if (SkelAnime_Update(&this->skelAnime)) { + DECR(this->timer); + + if (this->timer == 0) { + if ((EN_OKUTA_GET_TYPE(&this->actor) != EN_OKUTA_TYPE_BLUE_OCTOROK) || + (this->actor.xzDistToPlayer > 150.0f)) { + EnOkuta_SetupFloat(this); + } else { + EnOkuta_SetupShoot(this, play); + } + } else { + EnOkuta_SetupShoot(this, play); + } + } else { + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) { + curFrame = this->skelAnime.curFrame; + + if (curFrame < 13.0f) { + this->actor.world.pos.y = Math_SinF(0.2617889f * curFrame) * this->jumpHeight + this->actor.home.pos.y; + } + + if ((this->jumpHeight > 50.0f) && Animation_OnFrame(&this->skelAnime, 13.0f)) { + EnOkuta_SpawnSplash(this, play); + Actor_PlaySfx(&this->actor, NA_SE_EN_DAIOCTA_LAND); + } + } else { + this->actor.world.pos.y = EnOkuta_GetFloatHeight(this); + + if ((curFrame = this->skelAnime.curFrame) < 13.0f) { + Player* player = GET_PLAYER(play); + Vec3f targetPos; + s16 pitch; + + targetPos.x = player->actor.world.pos.x; + targetPos.y = player->actor.world.pos.y + 20.0f; + targetPos.z = player->actor.world.pos.z; + + pitch = Actor_WorldPitchTowardPoint(&this->actor, &targetPos); + pitch = CLAMP(pitch, -0x2AAA, 0); + + this->actor.shape.rot.x = Math_SinF(0.2617889f * curFrame) * pitch; + } + } + + if (Animation_OnFrame(&this->skelAnime, 6.0f)) { + EnOkuta_SpawnProjectile(this, play); + } + } + + if ((EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) && (this->actor.xzDistToPlayer < 160.0f)) { + EnOkuta_SetupHide(this); + } +} + +void EnOkuta_SetupDamaged(EnOkuta* this) { + Animation_MorphToPlayOnce(&this->skelAnime, &gOctorokHitAnim, -5.0f); + Actor_SetColorFilter(&this->actor, COLORFILTER_COLORFLAG_RED, 255, COLORFILTER_BUFFLAG_OPA, 11); + this->collider.base.acFlags &= ~AC_ON; + Actor_SetScale(&this->actor, 0.01f); + Actor_PlaySfx(&this->actor, NA_SE_EN_OCTAROCK_DEAD1); + this->actionFunc = EnOkuta_Damaged; +} + +void EnOkuta_Damaged(EnOkuta* this, PlayState* play) { + if (SkelAnime_Update(&this->skelAnime)) { + if (this->actor.colChkInfo.health == 0) { + EnOkuta_SetupDie(this); + } else { + this->collider.base.acFlags |= AC_ON; + EnOkuta_SetupFloat(this); + } + } + + Math_ApproachF(&this->actor.world.pos.y, this->actor.home.pos.y, 0.5f, 5.0f); +} + +void EnOkuta_SetupDie(EnOkuta* this) { + Animation_MorphToPlayOnce(&this->skelAnime, &gOctorokDieAnim, -3.0f); + this->timer = 0; + this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; + this->actionFunc = EnOkuta_Die; +} + +void EnOkuta_Die(EnOkuta* this, PlayState* play) { + static Vec3f sBubbleAccel = { 0.0f, -0.5f, 0.0f }; + static Color_RGBA8 sBubblePrimColor = { 255, 255, 255, 255 }; + static Color_RGBA8 sBubbleEnvColor = { 150, 150, 150, 0 }; + Vec3f velocity; + Vec3f pos; + s32 i; + + if (SkelAnime_Update(&this->skelAnime)) { + this->timer++; + } + + Math_ApproachF(&this->actor.world.pos.y, this->actor.home.pos.y, 0.5f, 5.0f); + + if (this->timer == 5) { + pos.x = this->actor.world.pos.x; + pos.y = this->actor.world.pos.y + 40.0f; + pos.z = this->actor.world.pos.z; + + velocity.x = 0.0f; + velocity.y = -0.5f; + velocity.z = 0.0f; + + EnOkuta_SpawnSmoke(&pos, &velocity, -20, play); + Actor_PlaySfx(&this->actor, NA_SE_EN_OCTAROCK_DEAD2); + } + + if (Animation_OnFrame(&this->skelAnime, 15.0f)) { + EnOkuta_SpawnSplash(this, play); + Actor_PlaySfx(&this->actor, NA_SE_EN_DAIOCTA_LAND); + } + + if (this->timer < 3) { + Actor_SetScale(&this->actor, ((this->timer * 0.25f) + 1.0f) * 0.01f); + return; + } + + if (this->timer < 6) { + Actor_SetScale(&this->actor, (1.5f - ((this->timer - 2) * 0.2333f)) * 0.01f); + return; + } + + if (this->timer < 11) { + Actor_SetScale(&this->actor, (((this->timer - 5) * 0.04f) + 0.8f) * 0.01f); + return; + } + + if (Math_StepToF(&this->actor.scale.x, 0.0f, 0.0005f)) { + SoundSource_PlaySfxAtFixedWorldPos(play, &this->actor.world.pos, 30, NA_SE_EN_COMMON_WATER_MID); + Item_DropCollectibleRandom(play, &this->actor, &this->actor.world.pos, 0xA0); + + for (i = 0; i < 20; i++) { + velocity.x = (Rand_ZeroOne() - 0.5f) * 7.0f; + velocity.y = Rand_ZeroOne() * 7.0f; + velocity.z = (Rand_ZeroOne() - 0.5f) * 7.0f; + EffectSsDtBubble_SpawnCustomColor(play, &this->actor.world.pos, &velocity, &sBubbleAccel, &sBubblePrimColor, + &sBubbleEnvColor, Rand_S16Offset(100, 50), 25, false); + } + + Actor_Kill(&this->actor); + } + + this->actor.scale.z = this->actor.scale.x; + this->actor.scale.y = this->actor.scale.x; +} + +void EnOkuta_SetupFrozen(EnOkuta* this, PlayState* play) { + this->timer = 10; + Actor_SetColorFilter(&this->actor, COLORFILTER_COLORFLAG_GRAY, COLORFILTER_INTENSITY_FLAG | 255, + COLORFILTER_BUFFLAG_OPA, 10); + this->actor.child = Actor_SpawnAsChild( + &play->actorCtx, &this->actor, play, ACTOR_OBJ_ICEBLOCK, this->actor.world.pos.x, + this->actor.world.pos.y + this->skelAnime.jointTable->y * this->actor.scale.y + 25.0f * this->headScale.y, + this->actor.world.pos.z, 0, this->actor.home.rot.y, 0, 3); + + if (this->actor.child != NULL) { + this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; + this->actor.flags |= ACTOR_FLAG_10; + this->actor.child->csId = this->actor.csId; + this->actionFunc = EnOkuta_FrozenInIceBlock; + } else { + EnOkuta_Freeze(this); + + if (Actor_ApplyDamage(&this->actor) == 0) { + Enemy_StartFinishingBlow(play, &this->actor); + this->collider.base.acFlags &= ~AC_ON; + this->timer = 3; + } + + this->actionFunc = EnOkuta_Frozen; + } +} + +/** + * Stops all movement and animation for the Octorok and waits until the ice block actor is killed. Afterwards, the + * Octorok will sink back to the water's surface and then start floating again. + */ +void EnOkuta_FrozenInIceBlock(EnOkuta* this, PlayState* play) { + this->actor.colorFilterTimer = 10; + + if ((this->actor.child == NULL) || (this->actor.child->update == NULL)) { + this->actor.flags |= ACTOR_FLAG_TARGETABLE; + + if (Math_StepToF(&this->actor.world.pos.y, this->actor.home.pos.y, 10.0f)) { + this->actor.flags &= ~ACTOR_FLAG_10; + EnOkuta_SetupFloat(this); + } + } +} + +/** + * Stops all movement and animation for the Octorok and waits 80 frames (or 3 frames if the attack that froze the + * Octorok also killed it). Afterwards, ice pieces will fly off the Octorok, and it will play its damaged animation. + * Note that this function is *not* responsible for handling the Octorok being encased in an ice block; that's handled + * by `EnOkuta_FrozenInIceBlock`. This behavior is incredibly difficult to trigger in-game and is unlikely to be seen by + * most players; see `EnOkuta_SetupFrozen` for more information. + */ +void EnOkuta_Frozen(EnOkuta* this, PlayState* play) { + DECR(this->timer); + + if (this->timer == 0) { + EnOkuta_Thaw(this, play); + EnOkuta_SetupDamaged(this); + } +} + +void EnOkuta_SetupSpin(EnOkuta* this) { + Animation_Change(&this->skelAnime, &gOctorokHitAnim, 1.0f, 0.0f, Animation_GetLastFrame(&gOctorokHitAnim) - 3.0f, + ANIMMODE_ONCE, -3.0f); + this->timer = 20; + this->actionFunc = EnOkuta_Spin; +} + +/** + * Spins in place for 20 frames while playing its hit animation, then makes the Octorok go back to floating. This + * function is only ever used by the unused blue Octorok variant; this is what the blue Octorok does instead of taking + * damage from the player's attacks. + */ +void EnOkuta_Spin(EnOkuta* this, PlayState* play) { + this->timer--; + Math_ScaledStepToS(&this->actor.shape.rot.x, 0, 0x400); + + if (SkelAnime_Update(&this->skelAnime)) { + Animation_Change(&this->skelAnime, &gOctorokHitAnim, 1.0f, 0.0f, + Animation_GetLastFrame(&gOctorokHitAnim) - 3.0f, ANIMMODE_ONCE, -3.0f); + } + + if (this->timer < 10) { + this->actor.shape.rot.y += (s16)(0x2000 * Math_SinF(this->timer * (M_PIf / 20.0f))); + } else { + this->actor.shape.rot.y += 0x2000; + } + + if (this->timer == 0) { + EnOkuta_SetupFloat(this); + } +} + +/** + * Handles all the behavior of the Octorok's projectile, including spinning the projectile around constantly and + * reflecting the projectile when it bounces off the player's shield . + */ +void EnOkuta_Projectile_Fly(EnOkuta* this, PlayState* play) { + Vec3f fragmentPos; + Player* player = GET_PLAYER(play); + Vec3s shieldRot; + + this->timer--; + + if (this->timer < 0) { + this->actor.velocity.y -= 0.5f; + this->actor.world.rot.x = + Math_Atan2S_XY(sqrtf(SQ(this->actor.velocity.x) + SQ(this->actor.velocity.z)), this->actor.velocity.y); + } + + this->actor.home.rot.z += 0x1554; + + if ((this->actor.bgCheckFlags & BGCHECKFLAG_WALL) || (this->actor.bgCheckFlags & BGCHECKFLAG_GROUND) || + (this->actor.bgCheckFlags & BGCHECKFLAG_CEILING) || (this->collider.base.atFlags & AT_HIT) || + (this->collider.base.acFlags & AC_HIT) || (this->collider.base.ocFlags1 & OC1_HIT) || + (this->actor.floorHeight == BGCHECK_Y_MIN)) { + if (player->currentShield == PLAYER_SHIELD_HEROS_SHIELD) { + if ((this->collider.base.atFlags & AT_HIT) && (this->collider.base.atFlags & AT_TYPE_ENEMY)) { + if (this->collider.base.atFlags & AT_BOUNCED) { + this->collider.base.atFlags &= ~(AT_HIT | AT_BOUNCED | AT_TYPE_ENEMY); + this->collider.base.atFlags |= AT_TYPE_PLAYER; + this->collider.info.toucher.dmgFlags = DMG_THROWN_OBJECT; + this->collider.info.toucher.damage = 2; + Matrix_MtxFToYXZRot(&player->shieldMf, &shieldRot, false); + this->actor.world.rot.y = shieldRot.y + 0x8000; + this->timer = 22; + return; + } + } + } + + fragmentPos.x = this->actor.world.pos.x; + fragmentPos.y = this->actor.world.pos.y + 11.0f; + fragmentPos.z = this->actor.world.pos.z; + EffectSsHahen_SpawnBurst(play, &fragmentPos, 6.0f, 0, 1, 2, 15, OBJECT_OKUTA, 10, gOctorokProjectileDL); + SoundSource_PlaySfxAtFixedWorldPos(play, &this->actor.world.pos, 20, NA_SE_EN_OCTAROCK_ROCK); + + if ((this->collider.base.atFlags & AT_HIT) && (this->collider.base.atFlags & AT_TYPE_ENEMY) && + !(this->collider.base.atFlags & AT_BOUNCED) && + (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_BLUE_PROJECTILE)) { + func_800B8D98(play, &this->actor, 8.0f, this->actor.world.rot.y, 6.0f); + } + + Actor_Kill(&this->actor); + return; + } + + if (this->timer == -300) { + Actor_Kill(&this->actor); + } +} + +/** + * Adjusts the scale of the Octorok's head based on their current action and their current animation frame. + */ +void EnOkuta_UpdateHeadScale(EnOkuta* this) { + f32 curFrame = this->skelAnime.curFrame; + + if (this->actionFunc == EnOkuta_Appear) { + if (curFrame < 8.0f) { + this->headScale.x = this->headScale.y = this->headScale.z = 1.0f; + } else if (curFrame < 10.0f) { + this->headScale.x = this->headScale.z = 1.0f; + this->headScale.y = ((curFrame - 7.0f) * 0.4f) + 1.0f; + } else if (curFrame < 14.0f) { + this->headScale.x = this->headScale.z = ((curFrame - 9.0f) * 0.075f) + 1.0f; + this->headScale.y = 1.8f - ((curFrame - 9.0f) * 0.25f); + } else { + this->headScale.x = this->headScale.z = 1.3f - ((curFrame - 13.0f) * 0.05f); + this->headScale.y = ((curFrame - 13.0f) * 0.0333f) + 0.8f; + } + } else if (this->actionFunc == EnOkuta_Hide) { + if (curFrame < 3.0f) { + this->headScale.y = 1.0f; + } else { + if (curFrame < 4.0f) { + this->headScale.y = (curFrame - 2.0f) + 1.0f; + } else { + this->headScale.y = 2.0f - ((curFrame - 3.0f) * 0.333f); + } + } + this->headScale.x = this->headScale.z = 1.0f; + } else if (this->actionFunc == EnOkuta_Shoot) { + if (curFrame < 5.0f) { + this->headScale.x = this->headScale.y = this->headScale.z = (curFrame * 0.125f) + 1.0f; + } else if (curFrame < 7.0f) { + this->headScale.x = this->headScale.y = this->headScale.z = 1.5f - ((curFrame - 4.0f) * 0.35f); + } else if (curFrame < 17.0f) { + this->headScale.x = this->headScale.z = ((curFrame - 6.0f) * 0.05f) + 0.8f; + this->headScale.y = 0.8f; + } else { + this->headScale.x = this->headScale.z = 1.3f - ((curFrame - 16.0f) * 0.1f); + this->headScale.y = ((curFrame - 16.0f) * 0.0666f) + 0.8f; + } + } else if (this->actionFunc == EnOkuta_Float) { + this->headScale.x = this->headScale.z = 1.0f; + this->headScale.y = Math_SinF((M_PIf / 16) * curFrame) * 0.2f + 1.0f; + } else { + this->headScale.x = this->headScale.y = this->headScale.z = 1.0f; + } +} + +void EnOkuta_UpdateDamage(EnOkuta* this, PlayState* play) { + if (!(this->collider.base.acFlags & AC_HIT)) { + return; + } + + this->collider.base.acFlags &= ~AC_HIT; + + if (this->drawDmgEffType != ACTOR_DRAW_DMGEFF_FROZEN_NO_SFX || + !(this->collider.info.acHitInfo->toucher.dmgFlags & 0xDB0B3)) { + Actor_SetDropFlag(&this->actor, &this->collider.info); + EnOkuta_Thaw(this, play); + + if (this->actor.colChkInfo.damageEffect == EN_OKUTA_DMGEFF_FREEZE) { + EnOkuta_SetupFrozen(this, play); + return; + } + + if (this->actor.colChkInfo.damageEffect == EN_OKUTA_DMGEFF_LIGHT_ORB) { + this->drawDmgEffAlpha = 4.0f; + this->drawDmgEffScale = 0.6f; + this->drawDmgEffType = ACTOR_DRAW_DMGEFF_LIGHT_ORBS; + Actor_Spawn(&play->actorCtx, play, ACTOR_EN_CLEAR_TAG, this->collider.info.bumper.hitPos.x, + this->collider.info.bumper.hitPos.y, this->collider.info.bumper.hitPos.z, 0, 0, 0, + CLEAR_TAG_PARAMS(CLEAR_TAG_LARGE_LIGHT_RAYS)); + } + + if (Actor_ApplyDamage(&this->actor) == 0) { + Enemy_StartFinishingBlow(play, &this->actor); + } + + EnOkuta_SetupDamaged(this); + } +} + +void EnOkuta_Update(Actor* thisx, PlayState* play2) { + PlayState* play = play2; + EnOkuta* this = THIS; + s32 pad[2]; + + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) { + EnOkuta_UpdateDamage(this, play); + } else if ((this->collider.base.atFlags & AT_HIT) || (this->collider.base.acFlags & AC_HIT)) { + if (this->collider.base.atFlags & AT_HIT) { + func_800B8D98(play, &this->actor, 8.0f, this->actor.world.rot.y, 6.0f); + } + + EnOkuta_SetupSpin(this); + } + + this->actionFunc(this, play); + + if (this->actionFunc != EnOkuta_FrozenInIceBlock) { + EnOkuta_UpdateHeadScale(this); + this->collider.dim.height = (sOctorokCylinderInit.dim.height * this->headScale.y - this->collider.dim.yShift) * + this->actor.scale.y * 100.0f; + Collider_UpdateCylinder(&this->actor, &this->collider); + + if (this->actionFunc == EnOkuta_Appear || this->actionFunc == EnOkuta_Hide) { + this->collider.dim.pos.y = this->actor.world.pos.y + this->skelAnime.jointTable->y * this->actor.scale.y; + this->collider.dim.radius = sOctorokCylinderInit.dim.radius * this->actor.scale.x * 100.0f; + } + + if (this->actor.draw != NULL) { + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_BLUE_OCTOROK) { + CollisionCheck_SetAT(play, &play->colChkCtx, &this->collider.base); + } + + if (this->collider.base.acFlags & AC_ON) { + CollisionCheck_SetAC(play, &play->colChkCtx, &this->collider.base); + } + + CollisionCheck_SetOC(play, &play->colChkCtx, &this->collider.base); + EnOkuta_SpawnRipple(this, play); + } + + Actor_SetFocus(&this->actor, 15.0f); + + 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) * 0.3f; + this->drawDmgEffScale = CLAMP_MAX(this->drawDmgEffScale, 0.6f); + return; + } + + if (!Math_StepToF(&this->drawDmgEffFrozenSteamScale, 0.6f, 15.0f * 0.001f)) { + Actor_PlaySfx_Flagged(&this->actor, NA_SE_EV_ICE_FREEZE - SFX_FLAG); + } + } + } +} + +void EnOkuta_Projectile_Update(Actor* thisx, PlayState* play) { + EnOkuta* this = THIS; + Player* player = GET_PLAYER(play); + s32 pad; + Vec3f prevPos; + s32 canRestorePrevPos = false; + + if (!(player->stateFlags1 & (PLAYER_STATE1_2 | PLAYER_STATE1_40 | PLAYER_STATE1_80 | PLAYER_STATE1_200 | + PLAYER_STATE1_10000000 | PLAYER_STATE1_20000000))) { + this->actionFunc(this, play); + Actor_MoveWithoutGravity(&this->actor); + Math_Vec3f_Copy(&prevPos, &this->actor.world.pos); + Actor_UpdateBgCheckInfo(play, &this->actor, 10.0f, 15.0f, 30.0f, + UPDBGCHECKINFO_FLAG_1 | UPDBGCHECKINFO_FLAG_2 | UPDBGCHECKINFO_FLAG_4); + + if ((this->actor.bgCheckFlags & BGCHECKFLAG_WALL) && + (SurfaceType_IsIgnoredByProjectiles(&play->colCtx, this->actor.wallPoly, this->actor.wallBgId))) { + canRestorePrevPos = true; + this->actor.bgCheckFlags &= ~BGCHECKFLAG_WALL; + } + + if ((this->actor.bgCheckFlags & BGCHECKFLAG_GROUND) && + (SurfaceType_IsIgnoredByProjectiles(&play->colCtx, this->actor.floorPoly, this->actor.floorBgId))) { + canRestorePrevPos = true; + this->actor.bgCheckFlags &= ~BGCHECKFLAG_GROUND; + } + + if (canRestorePrevPos && !(this->actor.bgCheckFlags & (BGCHECKFLAG_GROUND | BGCHECKFLAG_WALL))) { + Math_Vec3f_Copy(&this->actor.world.pos, &prevPos); + } + + Collider_UpdateCylinder(&this->actor, &this->collider); + this->actor.flags |= ACTOR_FLAG_1000000; + CollisionCheck_SetAT(play, &play->colChkCtx, &this->collider.base); + CollisionCheck_SetAC(play, &play->colChkCtx, &this->collider.base); + CollisionCheck_SetOC(play, &play->colChkCtx, &this->collider.base); + } +} + +/** + * Gets the scaling factor for animating the snout limb. If the limb is not being transformed, no scale value is + * returned. Returns true if the snout scale should be updated, false otherwise. The snout scale is returned via the + * `scale` parameter. + */ +s32 EnOkuta_GetSnoutScale(EnOkuta* this, f32 curFrame, Vec3f* scale) { + if (this->actionFunc == EnOkuta_Float) { + scale->z = scale->y = 1.0f; + scale->x = Math_SinF((M_PIf / 16) * curFrame) * 0.4f + 1.0f; + } else if (this->actionFunc == EnOkuta_Shoot) { + if (curFrame < 5.0f) { + scale->z = 1.0f; + scale->x = scale->y = (curFrame * 0.25f) + 1.0f; + } else if (curFrame < 7.0f) { + scale->z = (curFrame - 4.0f) * 0.5f + 1.0f; + scale->x = scale->y = 2.0f - (curFrame - 4.0f) * 0.5f; + } else { + scale->z = 2.0f - ((curFrame - 6.0f) * 0.0769f); + scale->x = scale->y = 1.0f; + } + } else if (this->actionFunc == EnOkuta_Die) { + if (curFrame >= 35.0f || curFrame < 25.0f) { + return false; + } + + if (curFrame < 27.0f) { + scale->z = 1.0f; + scale->x = scale->y = (curFrame - 24.0f) * 0.5f + 1.0f; + } else if (curFrame < 30.0f) { + scale->z = (curFrame - 26.0f) * 0.333f + 1.0f; + scale->x = scale->y = 2.0f - (curFrame - 26.0f) * 0.333f; + } else { + scale->z = 2.0f - ((curFrame - 29.0f) * 0.2f); + scale->x = scale->y = 1.0f; + } + } else { + return false; + } + + return true; +} + +s32 EnOkuta_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* pos, Vec3s* rot, Actor* thisx) { + EnOkuta* this = THIS; + s32 shouldScaleLimb = false; + Vec3f scale; + f32 curFrame = this->skelAnime.curFrame; + + if (this->actionFunc == EnOkuta_Die) { + curFrame += this->timer; + } + + if (limbIndex == OCTOROK_LIMB_HEAD) { + if (this->headScale.x != 1.0f || this->headScale.y != 1.0f || this->headScale.z != 1.0f) { + Math_Vec3f_Copy(&scale, &this->headScale); + shouldScaleLimb = true; + } + } else if (limbIndex == OCTOROK_LIMB_SNOUT) { + shouldScaleLimb = EnOkuta_GetSnoutScale(this, curFrame, &scale); + } + + if (shouldScaleLimb) { + Matrix_Scale(scale.x, scale.y, scale.z, MTXMODE_APPLY); + } + + return false; +} + +static s8 sLimbToBodyParts[OCTOROK_LIMB_MAX] = { + BODYPART_NONE, // OCTOROK_LIMB_NONE + EN_OKUTA_BODYPART_BODY, // OCTOROK_LIMB_BODY + BODYPART_NONE, // OCTOROK_LIMB_FRONT_LEFT_ARM_BASE + BODYPART_NONE, // OCTOROK_LIMB_FRONT_LEFT_ARM_MIDDLE + EN_OKUTA_BODYPART_FRONT_LEFT_ARM_END, // OCTOROK_LIMB_FRONT_LEFT_ARM_END + BODYPART_NONE, // OCTOROK_LIMB_FRONT_RIGHT_ARM_BASE + BODYPART_NONE, // OCTOROK_LIMB_FRONT_RIGHT_ARM_MIDDLE + EN_OKUTA_BODYPART_FRONT_RIGHT_ARM_END, // OCTOROK_LIMB_FRONT_RIGHT_ARM_END + BODYPART_NONE, // OCTOROK_LIMB_BACK_LEFT_ARM_BASE + BODYPART_NONE, // OCTOROK_LIMB_BACK_LEFT_ARM_MIDDLE + EN_OKUTA_BODYPART_BACK_LEFT_ARM_END, // OCTOROK_LIMB_BACK_LEFT_ARM_END + BODYPART_NONE, // OCTOROK_LIMB_BACK_RIGHT_ARM_BASE + BODYPART_NONE, // OCTOROK_LIMB_BACK_RIGHT_ARM_MIDDLE + EN_OKUTA_BODYPART_BACK_RIGHT_ARM_END, // OCTOROK_LIMB_BACK_RIGHT_ARM_END + EN_OKUTA_BODYPART_HEAD, // OCTOROK_LIMB_HEAD + EN_OKUTA_BODYPART_SNOUT, // OCTOROK_LIMB_SNOUT +}; + +/** + * The last three elements of `bodyPartsPos` are not tied to any particular limb and are instead used to control the + * placement of effects. The positions of these effects are offset by a certain amount from the Octorok's head limb. + */ +static Vec3f sEffectsBodyPartOffsets[3] = { + { 1500.0f, 1000.0f, 0.0f }, + { -1500.0f, 1000.0f, 0.0f }, + { 0.0f, 1500.0f, -2000.0f }, +}; + +void EnOkuta_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Actor* thisx) { + EnOkuta* this = THIS; + s32 bodyPartIndex = sLimbToBodyParts[limbIndex]; + s32 i; + + if (bodyPartIndex != BODYPART_NONE) { + if (bodyPartIndex == EN_OKUTA_BODYPART_SNOUT) { + Matrix_MultVecX(1500.0f, &this->bodyPartsPos[bodyPartIndex]); + } else if (bodyPartIndex == EN_OKUTA_BODYPART_HEAD) { + Matrix_MultVecY(2800.0f, &this->bodyPartsPos[bodyPartIndex]); + bodyPartIndex++; + + for (i = 0; i < ARRAY_COUNT(sEffectsBodyPartOffsets); i++) { + Matrix_MultVec3f(&sEffectsBodyPartOffsets[i], &this->bodyPartsPos[bodyPartIndex + i]); + } + } else { + Matrix_MultZero(&this->bodyPartsPos[bodyPartIndex]); + } + } +} + +void EnOkuta_Draw(Actor* thisx, PlayState* play) { + EnOkuta* this = THIS; + s32 pad; + Gfx* gfx; + + OPEN_DISPS(play->state.gfxCtx); + + gfx = POLY_OPA_DISP; + + gSPDisplayList(&gfx[0], gSetupDLs[SETUPDL_25]); + + if (EN_OKUTA_GET_TYPE(&this->actor) < EN_OKUTA_TYPE_PROJECTILE_BASE) { + if (EN_OKUTA_GET_TYPE(&this->actor) == EN_OKUTA_TYPE_RED_OCTOROK) { + gSPSegment(&gfx[1], 0x08, D_801AEFA0); + } else { + gSPSegment(&gfx[1], 0x08, gOctorokBlueMaterialDL); + } + + POLY_OPA_DISP = &gfx[2]; + SkelAnime_DrawOpa(play, this->skelAnime.skeleton, this->skelAnime.jointTable, EnOkuta_OverrideLimbDraw, + EnOkuta_PostLimbDraw, &this->actor); + } else { + Matrix_Mult(&play->billboardMtxF, MTXMODE_APPLY); + Matrix_RotateZS(this->actor.home.rot.z, MTXMODE_APPLY); + gSPMatrix(&gfx[1], Matrix_NewMtx(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + gSPDisplayList(&gfx[2], gOctorokProjectileDL); + POLY_OPA_DISP = &gfx[3]; + } + + CLOSE_DISPS(play->state.gfxCtx); + + Actor_DrawDamageEffects(play, &this->actor, this->bodyPartsPos, EN_OKUTA_BODYPART_MAX, + this->drawDmgEffScale * this->actor.scale.y * 100.0f, this->drawDmgEffFrozenSteamScale, + this->drawDmgEffAlpha, this->drawDmgEffType); +} diff --git a/src/overlays/actors/ovl_En_Okuta/z_en_okuta.h b/src/overlays/actors/ovl_En_Okuta/z_en_okuta.h index 93a68ae380..3f6b66cd71 100644 --- a/src/overlays/actors/ovl_En_Okuta/z_en_okuta.h +++ b/src/overlays/actors/ovl_En_Okuta/z_en_okuta.h @@ -2,16 +2,53 @@ #define Z_EN_OKUTA_H #include "global.h" +#include "objects/object_okuta/object_okuta.h" struct EnOkuta; typedef void (*EnOkutaActionFunc)(struct EnOkuta*, PlayState*); +#define EN_OKUTA_GET_NUM_CONSECUTIVE_PROJECTILES(thisx) (((thisx)->params >> 8) & 0xFF) +#define EN_OKUTA_GET_TYPE(thisx) ((thisx)->params) + +typedef enum EnOkutaType { + /* 0 */ EN_OKUTA_TYPE_RED_OCTOROK, + /* 1 */ EN_OKUTA_TYPE_BLUE_OCTOROK, // Unused in the final game. This variant is invincible to all attacks and can chase the player underwater if they dive. + /* 16 */ EN_OKUTA_TYPE_PROJECTILE_BASE = 16, + /* 16 */ EN_OKUTA_TYPE_RED_PROJECTILE = EN_OKUTA_TYPE_RED_OCTOROK + EN_OKUTA_TYPE_PROJECTILE_BASE, + /* 17 */ EN_OKUTA_TYPE_BLUE_PROJECTILE = EN_OKUTA_TYPE_BLUE_OCTOROK + EN_OKUTA_TYPE_PROJECTILE_BASE +} EnOkutaType; + +typedef enum EnOkutaBodyPart { + /* 0 */ EN_OKUTA_BODYPART_BODY, + /* 1 */ EN_OKUTA_BODYPART_FRONT_LEFT_ARM_END, + /* 2 */ EN_OKUTA_BODYPART_FRONT_RIGHT_ARM_END, + /* 3 */ EN_OKUTA_BODYPART_BACK_LEFT_ARM_END, + /* 4 */ EN_OKUTA_BODYPART_BACK_RIGHT_ARM_END, + /* 5 */ EN_OKUTA_BODYPART_SNOUT, + /* 6 */ EN_OKUTA_BODYPART_HEAD, + /* 7 */ EN_OKUTA_BODYPART_EFFECTS_1, + /* 8 */ EN_OKUTA_BODYPART_EFFECTS_2, + /* 9 */ EN_OKUTA_BODYPART_EFFECTS_3, + /* 10 */ EN_OKUTA_BODYPART_MAX +} EnOkutaBodyPart; + typedef struct EnOkuta { /* 0x000 */ Actor actor; - /* 0x144 */ char unk_144[0x44]; + /* 0x144 */ SkelAnime skelAnime; /* 0x188 */ EnOkutaActionFunc actionFunc; - /* 0x18C */ char unk_18C[0x1A8]; + /* 0x18C */ u8 drawDmgEffType; + /* 0x18E */ s16 timer; + /* 0x190 */ s16 numConsecutiveProjectiles; // when the Octorok starts shooting projectiles, it will shoot this many projectiles in a row before stopping + /* 0x192 */ Vec3s jointTable[OCTOROK_LIMB_MAX]; + /* 0x1F2 */ Vec3s morphTable[OCTOROK_LIMB_MAX]; + /* 0x254 */ f32 drawDmgEffAlpha; + /* 0x258 */ f32 drawDmgEffScale; + /* 0x25C */ f32 drawDmgEffFrozenSteamScale; + /* 0x260 */ f32 jumpHeight; + /* 0x264 */ Vec3f headScale; + /* 0x270 */ Vec3f bodyPartsPos[EN_OKUTA_BODYPART_MAX]; + /* 0x2E8 */ ColliderCylinder collider; } EnOkuta; // size = 0x334 #endif // Z_EN_OKUTA_H diff --git a/src/overlays/actors/ovl_En_Syateki_Okuta/z_en_syateki_okuta.c b/src/overlays/actors/ovl_En_Syateki_Okuta/z_en_syateki_okuta.c index f4f8f3ffa0..7f650a6ad1 100644 --- a/src/overlays/actors/ovl_En_Syateki_Okuta/z_en_syateki_okuta.c +++ b/src/overlays/actors/ovl_En_Syateki_Okuta/z_en_syateki_okuta.c @@ -124,11 +124,11 @@ void EnSyatekiOkuta_Destroy(Actor* thisx, PlayState* play) { /** * Spawns the puff of smoke that appears when the Octorok disappears when it dies. */ -void EnSyatekiOkuta_SpawnDust(Vec3f* pos, Vec3f* velocity, s16 scaleStep, PlayState* play) { - static Color_RGBA8 sDustPrimColor = { 255, 255, 255, 255 }; - static Color_RGBA8 sDustEnvColor = { 150, 150, 150, 255 }; +void EnSyatekiOkuta_SpawnSmoke(Vec3f* pos, Vec3f* velocity, s16 scaleStep, PlayState* play) { + static Color_RGBA8 sSmokePrimColor = { 255, 255, 255, 255 }; + static Color_RGBA8 sSmokeEnvColor = { 150, 150, 150, 255 }; - func_800B0DE0(play, pos, velocity, &gZeroVec3f, &sDustPrimColor, &sDustEnvColor, 400, scaleStep); + func_800B0DE0(play, pos, velocity, &gZeroVec3f, &sSmokePrimColor, &sSmokeEnvColor, 400, scaleStep); } /** @@ -308,7 +308,7 @@ void EnSyatekiOkuta_Die(EnSyatekiOkuta* this, PlayState* play) { velocity.x = 0.0f; velocity.y = -0.5f; velocity.z = 0.0f; - EnSyatekiOkuta_SpawnDust(&pos, &velocity, -20, play); + EnSyatekiOkuta_SpawnSmoke(&pos, &velocity, -20, play); Actor_PlaySfx(&this->actor, NA_SE_EN_OCTAROCK_DEAD2); } @@ -502,8 +502,9 @@ void EnSyatekiOkuta_UpdateHeadScale(EnSyatekiOkuta* this) { } /** - * Returns true if the snout scale should be updated, false otherwise. The snout scale is returned via the scale - * parameter. + * Gets the scaling factor for animating the snout limb. If the limb is not being transformed, no scale value is + * returned. Returns true if the snout scale should be updated, false otherwise. The snout scale is returned via the + * `scale` parameter. */ s32 EnSyatekiOkuta_GetSnoutScale(EnSyatekiOkuta* this, f32 curFrame, Vec3f* scale) { if (this->actionFunc == EnSyatekiOkuta_Appear) { diff --git a/tools/disasm/functions.txt b/tools/disasm/functions.txt index 3b259bea77..fdc06d2f26 100644 --- a/tools/disasm/functions.txt +++ b/tools/disasm/functions.txt @@ -5060,41 +5060,41 @@ 0x8086D898:("EnPametfrog_Draw",), 0x8086DE20:("EnOkuta_Init",), 0x8086E058:("EnOkuta_Destroy",), - 0x8086E084:("func_8086E084",), - 0x8086E0F0:("func_8086E0F0",), - 0x8086E168:("func_8086E168",), - 0x8086E214:("func_8086E214",), - 0x8086E27C:("func_8086E27C",), - 0x8086E2C0:("func_8086E2C0",), - 0x8086E378:("func_8086E378",), - 0x8086E3B8:("func_8086E3B8",), - 0x8086E4FC:("func_8086E4FC",), - 0x8086E52C:("func_8086E52C",), - 0x8086E5E8:("func_8086E5E8",), - 0x8086E658:("func_8086E658",), - 0x8086E7A8:("func_8086E7A8",), - 0x8086E7E8:("func_8086E7E8",), - 0x8086E8E8:("func_8086E8E8",), - 0x8086E948:("func_8086E948",), - 0x8086EAE0:("func_8086EAE0",), - 0x8086EC00:("func_8086EC00",), - 0x8086EE8C:("func_8086EE8C",), - 0x8086EF14:("func_8086EF14",), - 0x8086EF90:("func_8086EF90",), - 0x8086EFE8:("func_8086EFE8",), - 0x8086F2FC:("func_8086F2FC",), - 0x8086F434:("func_8086F434",), - 0x8086F4B0:("func_8086F4B0",), - 0x8086F4F4:("func_8086F4F4",), - 0x8086F57C:("func_8086F57C",), - 0x8086F694:("func_8086F694",), - 0x8086F8FC:("func_8086F8FC",), - 0x8086FCA4:("func_8086FCA4",), + 0x8086E084:("EnOkuta_Freeze",), + 0x8086E0F0:("EnOkuta_Thaw",), + 0x8086E168:("EnOkuta_SpawnBubbles",), + 0x8086E214:("EnOkuta_SpawnSmoke",), + 0x8086E27C:("EnOkuta_SpawnSplash",), + 0x8086E2C0:("EnOkuta_SpawnRipple",), + 0x8086E378:("EnOkuta_GetFloatHeight",), + 0x8086E3B8:("EnOkuta_SpawnProjectile",), + 0x8086E4FC:("EnOkuta_SetupWaitToAppear",), + 0x8086E52C:("EnOkuta_WaitToAppear",), + 0x8086E5E8:("EnOkuta_SetupAppear",), + 0x8086E658:("EnOkuta_Appear",), + 0x8086E7A8:("EnOkuta_SetupHide",), + 0x8086E7E8:("EnOkuta_Hide",), + 0x8086E8E8:("EnOkuta_SetupFloat",), + 0x8086E948:("EnOkuta_Float",), + 0x8086EAE0:("EnOkuta_SetupShoot",), + 0x8086EC00:("EnOkuta_Shoot",), + 0x8086EE8C:("EnOkuta_SetupDamaged",), + 0x8086EF14:("EnOkuta_Damaged",), + 0x8086EF90:("EnOkuta_SetupDie",), + 0x8086EFE8:("EnOkuta_Die",), + 0x8086F2FC:("EnOkuta_SetupFrozen",), + 0x8086F434:("EnOkuta_FrozenInIceBlock",), + 0x8086F4B0:("EnOkuta_Frozen",), + 0x8086F4F4:("EnOkuta_SetupSpin",), + 0x8086F57C:("EnOkuta_Spin",), + 0x8086F694:("EnOkuta_Projectile_Fly",), + 0x8086F8FC:("EnOkuta_UpdateHeadScale",), + 0x8086FCA4:("EnOkuta_UpdateDamage",), 0x8086FDE0:("EnOkuta_Update",), - 0x808700C0:("func_808700C0",), - 0x80870254:("func_80870254",), - 0x808704DC:("func_808704DC",), - 0x808705C8:("func_808705C8",), + 0x808700C0:("EnOkuta_Projectile_Update",), + 0x80870254:("EnOkuta_GetSnoutScale",), + 0x808704DC:("EnOkuta_OverrideLimbDraw",), + 0x808705C8:("EnOkuta_PostLimbDraw",), 0x808706E0:("EnOkuta_Draw",), 0x80870DB0:("EnBom_Init",), 0x80870FF8:("EnBom_Destroy",), @@ -10217,7 +10217,7 @@ 0x80A35DDC:("ObjBell_Draw",), 0x80A35FF0:("EnSyatekiOkuta_Init",), 0x80A3611C:("EnSyatekiOkuta_Destroy",), - 0x80A36148:("EnSyatekiOkuta_SpawnDust",), + 0x80A36148:("EnSyatekiOkuta_SpawnSmoke",), 0x80A361B0:("EnSyatekiOkuta_SpawnSplash",), 0x80A361F4:("EnSyatekiOkuta_IsHiddenByAnotherOctorok",), 0x80A36260:("EnSyatekiOkuta_SetupAttachToShootingGalleryMan",), diff --git a/tools/disasm/variables.txt b/tools/disasm/variables.txt index 1055a990a4..dce154a10d 100644 --- a/tools/disasm/variables.txt +++ b/tools/disasm/variables.txt @@ -5389,23 +5389,20 @@ 0x8086DA94:("D_8086DA94","f32","",0x4), 0x8086DA98:("D_8086DA98","f32","",0x4), 0x8086DA9C:("D_8086DA9C","f32","",0x4), - 0x80870870:("D_80870870","UNK_TYPE1","",0x1), - 0x80870880:("En_Okuta_InitVars","UNK_TYPE1","",0x1), - 0x808708A0:("D_808708A0","UNK_TYPE1","",0x1), - 0x808708CC:("D_808708CC","UNK_TYPE1","",0x1), - 0x808708EC:("D_808708EC","UNK_TYPE2","",0x2), - 0x808708EE:("D_808708EE","UNK_TYPE2","",0x2), - 0x808708F8:("D_808708F8","UNK_TYPE1","",0x1), - 0x80870900:("D_80870900","UNK_TYPE1","",0x1), - 0x80870920:("D_80870920","UNK_TYPE1","",0x1), - 0x80870928:("D_80870928","UNK_TYPE1","",0x1), - 0x8087092C:("D_8087092C","UNK_TYPE1","",0x1), - 0x80870930:("D_80870930","UNK_TYPE1","",0x1), - 0x8087093C:("D_8087093C","UNK_TYPE1","",0x1), - 0x80870940:("D_80870940","UNK_TYPE1","",0x1), - 0x80870944:("D_80870944","UNK_TYPE1","",0x1), - 0x80870954:("D_80870954","UNK_TYPE1","",0x1), - 0x80870978:("D_80870978","UNK_TYPE1","",0x1), + 0x80870870:("gOctorokBlueMaterialDL","Gfx","[2]",0x10), + 0x80870880:("En_Okuta_InitVars","ActorInit","",0x20), + 0x808708A0:("sProjectileCylinderInit","ColliderCylinderInit","",0x2C), + 0x808708CC:("sOctorokCylinderInit","ColliderCylinderInit","",0x2C), + 0x808708F8:("sColChkInfoInit","CollisionCheckInfoInit","",0x4), + 0x80870900:("sDamageTable","DamageTable","",0x20), + 0x80870920:("sInitChain","InitChainEntry","[2]",0x8), + 0x80870928:("sSmokePrimColor","Color_RGBA8","",0x4), + 0x8087092C:("sSmokeEnvColor","Color_RGBA8","",0x4), + 0x80870930:("sBubbleAccel","Vec3f","",0xC), + 0x8087093C:("sBubblePrimColor","Color_RGBA8","",0x4), + 0x80870940:("sBubbleEnvColor","Color_RGBA8","",0x4), + 0x80870944:("sLimbToBodyParts","s8","[16]",0x10), + 0x80870954:("sEffectsBodyPartOffsets","Vec3f","[3]",0x24), 0x80870980:("D_80870980","f32","",0x4), 0x80870984:("D_80870984","f32","",0x4), 0x80870988:("D_80870988","f32","",0x4), @@ -11058,8 +11055,8 @@ 0x80A37A88:("D_80A37A88","UNK_TYPE1","",0x1), 0x80A37B08:("D_80A37B08","UNK_TYPE1","",0x1), 0x80A37B88:("D_80A37B88","UNK_TYPE1","",0x1), - 0x80A37B90:("sDustPrimColor","UNK_TYPE1","",0x1), - 0x80A37B94:("sDustEnvColor","UNK_TYPE1","",0x1), + 0x80A37B90:("sSmokePrimColor","UNK_TYPE1","",0x1), + 0x80A37B94:("sSmokeEnvColor","UNK_TYPE1","",0x1), 0x80A37B98:("sBubbleAccel","UNK_TYPE1","",0x1), 0x80A37BA4:("sBubblePrimColor","UNK_TYPE1","",0x1), 0x80A37BA8:("sBubbleEnvColor","UNK_TYPE1","",0x1),