diff --git a/assets/xml/objects/object_kitan.xml b/assets/xml/objects/object_kitan.xml index cd7db0005b..0021986804 100644 --- a/assets/xml/objects/object_kitan.xml +++ b/assets/xml/objects/object_kitan.xml @@ -1,8 +1,9 @@  + - - - + + + @@ -29,26 +30,26 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/spec b/spec index 2626fee0df..ed0514bf5c 100644 --- a/spec +++ b/spec @@ -4734,8 +4734,7 @@ beginseg name "ovl_En_Kitan" compress include "build/src/overlays/actors/ovl_En_Kitan/z_en_kitan.o" - include "build/data/ovl_En_Kitan/ovl_En_Kitan.data.o" - include "build/data/ovl_En_Kitan/ovl_En_Kitan.reloc.o" + include "build/src/overlays/actors/ovl_En_Kitan/ovl_En_Kitan_reloc.o" endseg beginseg diff --git a/src/overlays/actors/ovl_En_Kitan/z_en_kitan.c b/src/overlays/actors/ovl_En_Kitan/z_en_kitan.c index 83635f6c25..adc1bc1c2c 100644 --- a/src/overlays/actors/ovl_En_Kitan/z_en_kitan.c +++ b/src/overlays/actors/ovl_En_Kitan/z_en_kitan.c @@ -1,7 +1,7 @@ /* * File: z_en_kitan.c * Overlay: ovl_En_Kitan - * Description: + * Description: Keaton */ #include "z_en_kitan.h" @@ -13,8 +13,11 @@ void EnKitan_Init(Actor* thisx, PlayState* play); void EnKitan_Destroy(Actor* thisx, PlayState* play); void EnKitan_Update(Actor* thisx, PlayState* play); +void EnKitan_Draw(Actor* thisx, PlayState* play); + +void EnKitan_Talk(EnKitan* this, PlayState* play); +void EnKitan_WaitToAppear(EnKitan* this, PlayState* play); -#if 0 ActorInit En_Kitan_InitVars = { /**/ ACTOR_EN_KITAN, /**/ ACTORCAT_NPC, @@ -27,50 +30,364 @@ ActorInit En_Kitan_InitVars = { /**/ NULL, }; -// static ColliderCylinderInit sCylinderInit = { -static ColliderCylinderInit D_80C09D50 = { - { COLTYPE_NONE, AT_NONE, AC_ON | AC_TYPE_ENEMY, OC1_ON | OC1_TYPE_ALL, OC2_TYPE_1, COLSHAPE_CYLINDER, }, - { ELEMTYPE_UNK0, { 0x00000000, 0x00, 0x00 }, { 0xF7CFFFFF, 0x00, 0x00 }, TOUCH_NONE | TOUCH_SFX_NORMAL, BUMP_ON, OCELEM_ON, }, +static ColliderCylinderInit sCylinderInit = { + { + COLTYPE_NONE, + AT_NONE, + AC_ON | AC_TYPE_ENEMY, + OC1_ON | OC1_TYPE_ALL, + OC2_TYPE_1, + COLSHAPE_CYLINDER, + }, + { + ELEMTYPE_UNK0, + { 0x00000000, 0x00, 0x00 }, + { 0xF7CFFFFF, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON, + OCELEM_ON, + }, { 20, 40, 0, { 0, 0, 0 } }, }; -#endif +void EnKitan_Init(Actor* thisx, PlayState* play) { + EnKitan* this = THIS; + s32 pad; -extern ColliderCylinderInit D_80C09D50; + Actor_SetScale(&this->actor, 0.0f); + this->actionFunc = EnKitan_WaitToAppear; -extern UNK_TYPE D_06000CE8; -extern UNK_TYPE D_06002770; + ActorShape_Init(&this->actor.shape, 0.0f, ActorShadow_DrawCircle, 12.0f); + SkelAnime_InitFlex(play, &this->skelAnime, &gKeatonSkel, &gKeatonIdleAnim, this->jointTable, this->morphTable, + KEATON_LIMB_MAX); + Animation_PlayLoop(&this->skelAnime, &gKeatonIdleAnim); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/EnKitan_Init.s") + Collider_InitAndSetCylinder(play, &this->collider, &this->actor, &sCylinderInit); + this->actor.colChkInfo.mass = MASS_IMMOVABLE; + Collider_UpdateCylinder(&this->actor, &this->collider); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/EnKitan_Destroy.s") + this->actor.velocity.y = -9.0f; + this->actor.terminalVelocity = -9.0f; + this->actor.gravity = -1.0f; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C0923C.s") + if ((Player_GetMask(play) != PLAYER_MASK_KEATON) || + Flags_GetCollectible(play, ENKITAN_GET_COLLECT_FLAG(&this->actor))) { + Actor_Kill(&this->actor); + return; + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09390.s") + this->timer = 120; + this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09418.s") +void EnKitan_Destroy(Actor* thisx, PlayState* play) { + EnKitan* this = THIS; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C094A8.s") + Collider_DestroyCylinder(play, &this->collider); +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09518.s") +void EnKitan_SpawnEffects(EnKitan* this, PlayState* play, s32 numEffects) { + static Color_RGBA8 sEffPrimColor = { 255, 255, 255, 255 }; + static Color_RGBA8 sEffEnvColor = { 255, 255, 200, 255 }; + s32 i; + Vec3f accel; + Vec3f velocity; + Vec3f pos; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C095C8.s") + pos.x = this->actor.world.pos.x; + pos.y = this->actor.world.pos.y; + pos.z = this->actor.world.pos.z; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09648.s") + for (i = 0; i < numEffects; i++) { + velocity.x = Rand_CenteredFloat(10.0f); + velocity.y = Rand_ZeroFloat(6.0f); + velocity.z = Rand_CenteredFloat(10.0f); + accel.x = -velocity.x * 0.05f; + accel.y = 0.1f; + accel.z = -velocity.x * 0.05f; + func_800B0F18(play, &pos, &velocity, &accel, &sEffPrimColor, &sEffEnvColor, 400, 20, 20); + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09708.s") +s32 EnKitan_CanTalk(EnKitan* this, PlayState* play) { + Player* player = GET_PLAYER(play); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09990.s") + if (player->stateFlags1 & PLAYER_STATE1_800000) { + return false; + } + if (Player_IsFacingActor(&this->actor, 0x3000, play) && Actor_IsFacingPlayer(&this->actor, 0x3000) && + (this->actor.xzDistToPlayer < 120.0f)) { + return true; + } + return false; +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09AA4.s") +u16 EnKitan_GetQuestionMessageId(EnKitan* this) { + s32 i = 0; -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09B50.s") + while (true) { + s32 rand = Rand_ZeroFloat(30.0f); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/EnKitan_Update.s") + // Keep track of which questions have already been asked with a bitset + if (!(this->textBitSet & (1 << rand))) { + this->textBitSet |= 1 << rand; + // 0x04B6 is the start of the question + answer choice textboxes, each question textbox is followed by the + // choice textbox containing the answer choices + return 0x4B6 + (rand * 2); + } -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09C74.s") + i++; + if (i > 1000) { + // There's an assert(false) here in the debug version to catch any unforeseen issues in testing. It is + // assumed that if this function is called there is always at least one question available to be selected. + } + } +} -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09C90.s") +void EnKitan_Leave(EnKitan* this, PlayState* play) { + SkelAnime_Update(&this->skelAnime); -#pragma GLOBAL_ASM("asm/non_matchings/overlays/ovl_En_Kitan/func_80C09CD0.s") + if (this->timer > 0) { + // Scale down + this->timer--; + this->actor.scale.x *= 0.7f; + Actor_SetScale(&this->actor, this->actor.scale.x); + } else { + Actor_Kill(&this->actor); + } +} + +void EnKitan_TalkAfterGivingPrize(EnKitan* this, PlayState* play) { + s32 pad; + + SkelAnime_Update(&this->skelAnime); + + if (Actor_TalkOfferAccepted(&this->actor, &play->state)) { + this->actionFunc = EnKitan_Talk; + Message_ContinueTextbox(play, 0x04B5); + this->actor.flags &= ~ACTOR_FLAG_10000; + Animation_MorphToLoop(&this->skelAnime, &gKeatonChuckleAnim, -5.0f); + } else { + Actor_OfferTalkExchange(&this->actor, play, 1000.0f, 1000.0f, PLAYER_IA_MINUS1); + } +} + +void EnKitan_WaitForPrizeTextboxClosed(EnKitan* this, PlayState* play) { + SkelAnime_Update(&this->skelAnime); + + if (Actor_TextboxIsClosing(&this->actor, play)) { + this->actor.flags |= ACTOR_FLAG_10000; + this->actionFunc = EnKitan_TalkAfterGivingPrize; + Actor_OfferTalkExchange(&this->actor, play, 1000.0f, 1000.0f, PLAYER_IA_MINUS1); + } +} + +void EnKitan_OfferPrize(EnKitan* this, PlayState* play) { + SkelAnime_Update(&this->skelAnime); + + if (Actor_HasParent(&this->actor, play)) { + this->actor.parent = NULL; + this->actionFunc = EnKitan_WaitForPrizeTextboxClosed; + SET_WEEKEVENTREG(WEEKEVENTREG_79_80); + return; + } + + // Reward the player with a heart piece, or a red rupee if the heart piece was already obtained. + if (CHECK_WEEKEVENTREG(WEEKEVENTREG_79_80)) { + Actor_OfferGetItem(&this->actor, play, GI_RUPEE_RED, 2000.0f, 1000.0f); + } else { + Actor_OfferGetItem(&this->actor, play, GI_HEART_PIECE, 2000.0f, 1000.0f); + } +} + +void EnKitan_Talk(EnKitan* this, PlayState* play) { + if (SkelAnime_Update(&this->skelAnime)) { + Animation_MorphToLoop(&this->skelAnime, &gKeatonIdleAnim, -10.0f); + if (play->msgCtx.currentTextId != 0x04B4) { + // If the quiz is ongoing, select a question + Message_ContinueTextbox(play, EnKitan_GetQuestionMessageId(this)); + } + } + + switch (Message_GetState(&play->msgCtx)) { + case TEXT_STATE_CHOICE: + if (!Message_ShouldAdvance(play)) { + break; + } + + if ((play->msgCtx.choiceIndex + 1) == play->msgCtx.unk1206C) { + // Correct answer, continue quiz or end if enough questions have been answered correctly + Audio_PlaySfx(NA_SE_SY_QUIZ_CORRECT); + + // Here the timer is being used as a counter for number of correct answers + this->timer++; + if (this->timer < 5) { + play->msgCtx.msgLength = 0; + } else { + // Enough questions have been answered, continue to prize + this->timer = 0; + this->textBitSet = 0; + Message_ContinueTextbox(play, 0x04B4); + } + Animation_MorphToPlayOnce(&this->skelAnime, &gKeatonCelebrateAnim, -5.0f); + } else { + // Wrong answer, end quiz + Audio_PlaySfx(NA_SE_SY_QUIZ_INCORRECT); + Animation_MorphToLoop(&this->skelAnime, &gKeatonChuckleAnim, -5.0f); + Message_ContinueTextbox(play, 0x04B3); + this->timer = 0; + this->textBitSet = 0; + } + break; + + case TEXT_STATE_EVENT: + if (!Message_ShouldAdvance(play)) { + break; + } + + switch (play->msgCtx.currentTextId) { + case 0x04B0: + case 0x04B1: + // Intro text + Message_ContinueTextbox(play, play->msgCtx.currentTextId + 1); + break; + + case 0x04B2: + // Quiz begins + Animation_MorphToLoop(&this->skelAnime, &gKeatonIdleAnim, -5.0f); + Message_ContinueTextbox(play, EnKitan_GetQuestionMessageId(this)); + break; + + case 0x04B4: + // Won the quiz + Message_CloseTextbox(play); + this->actionFunc = EnKitan_OfferPrize; + EnKitan_OfferPrize(this, play); + break; + + case 0x04B3: + // Answered a question incorrectly, stop + SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_FANFARE, 0); + FALLTHROUGH; + case 0x04B5: + // Keaton leaving + Message_CloseTextbox(play); + this->actionFunc = EnKitan_Leave; + this->timer = 4; + EnKitan_SpawnEffects(this, play, 30); + SoundSource_PlaySfxAtFixedWorldPos(play, &this->actor.world.pos, 30, NA_SE_EN_NPC_FADEAWAY); + Flags_SetCollectible(play, ENKITAN_GET_COLLECT_FLAG(&this->actor)); + break; + + default: + if (!(play->msgCtx.currentTextId & 1)) { + // Even-numbered textboxes are question textboxes + // The following textbox contains the associated answer choices for this question + Message_ContinueTextbox(play, play->msgCtx.currentTextId + 1); + } + break; + } + break; + + default: + break; + } +} + +void EnKitan_WaitForPlayer(EnKitan* this, PlayState* play) { + SkelAnime_Update(&this->skelAnime); + + if (Actor_TalkOfferAccepted(&this->actor, &play->state)) { + // Began talking + this->actionFunc = EnKitan_Talk; + Message_StartTextbox(play, 0x04B0, &this->actor); + this->timer = 0; + Animation_MorphToLoop(&this->skelAnime, &gKeatonChuckleAnim, -5.0f); + Audio_PlayFanfare(NA_BGM_KEATON_QUIZ); + return; + } + + if ((this->timer <= 0) || (Player_GetMask(play) != PLAYER_MASK_KEATON)) { + // If the player does not talk quickly enough or the player isn't wearing the keaton mask, leave + this->actionFunc = EnKitan_Leave; + this->timer = 4; + EnKitan_SpawnEffects(this, play, 30); + SoundSource_PlaySfxAtFixedWorldPos(play, &this->actor.world.pos, 30, NA_SE_EN_NPC_FADEAWAY); + return; + } + + if (EnKitan_CanTalk(this, play)) { + // Broadcast talk request for the player to accept + Actor_OfferTalk(&this->actor, play, 130.0f); + this->timer--; + } +} + +void EnKitan_Appear(EnKitan* this, PlayState* play) { + SkelAnime_Update(&this->skelAnime); + + if (this->timer > 0) { + // Scale up + this->timer--; + this->actor.scale.x = this->actor.scale.x * 0.3f + 0.0105f; + Actor_SetScale(&this->actor, this->actor.scale.x); + return; + } + + // Done scaling, continue + Actor_SetScale(&this->actor, 0.015f); + this->actionFunc = EnKitan_WaitForPlayer; + this->actor.flags |= ACTOR_FLAG_TARGETABLE; + this->timer = 600; +} + +void EnKitan_WaitToAppear(EnKitan* this, PlayState* play) { + if (this->timer > 0) { + this->timer--; + return; + } + EnKitan_SpawnEffects(this, play, 30); + Actor_PlaySfx(&this->actor, NA_SE_EN_NPC_APPEAR); + this->actor.draw = EnKitan_Draw; + this->actionFunc = EnKitan_Appear; + this->timer = 20; + this->actor.shape.rot.y = this->actor.yawTowardsPlayer; + this->actor.world.rot.y = this->actor.shape.rot.y; +} + +void EnKitan_Update(Actor* thisx, PlayState* play) { + EnKitan* this = THIS; + + if (this->actor.draw != NULL) { + CollisionCheck_SetOC(play, &play->colChkCtx, &this->collider.base); + } + Actor_MoveWithGravity(&this->actor); + Actor_UpdateBgCheckInfo(play, &this->actor, 40.0f, 25.0f, 40.0f, UPDBGCHECKINFO_FLAG_1 | UPDBGCHECKINFO_FLAG_4); + + // Face the player + Math_SmoothStepToS(&this->actor.shape.rot.y, this->actor.yawTowardsPlayer, 2, 0x1000, 0x200); + this->actor.world.rot.y = this->actor.shape.rot.y; + + this->actionFunc(this, play); +} + +s32 EnKitan_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* pos, Vec3s* rot, Actor* thisx) { + return false; +} + +void EnKitan_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Actor* thisx) { + static Vec3f sFocusOffset = { 0.0f, 0.0f, 0.0f }; + EnKitan* this = THIS; + + if (limbIndex == KEATON_LIMB_RIGHT_SHOULDER) { + Matrix_MultVec3f(&sFocusOffset, &this->actor.focus.pos); + } +} + +void EnKitan_Draw(Actor* thisx, PlayState* play) { + EnKitan* this = THIS; + + Gfx_SetupDL37_Opa(play->state.gfxCtx); + SkelAnime_DrawFlexOpa(play, this->skelAnime.skeleton, this->skelAnime.jointTable, this->skelAnime.dListCount, + EnKitan_OverrideLimbDraw, EnKitan_PostLimbDraw, &this->actor); +} diff --git a/src/overlays/actors/ovl_En_Kitan/z_en_kitan.h b/src/overlays/actors/ovl_En_Kitan/z_en_kitan.h index 9d2ae096b0..99ee4a55ba 100644 --- a/src/overlays/actors/ovl_En_Kitan/z_en_kitan.h +++ b/src/overlays/actors/ovl_En_Kitan/z_en_kitan.h @@ -2,6 +2,9 @@ #define Z_EN_KITAN_H #include "global.h" +#include "objects/object_kitan/object_kitan.h" + +#define ENKITAN_GET_COLLECT_FLAG(thisx) (((thisx)->params & 0xFE00) >> 9) struct EnKitan; @@ -9,7 +12,13 @@ typedef void (*EnKitanActionFunc)(struct EnKitan*, PlayState*); typedef struct EnKitan { /* 0x000 */ Actor actor; - /* 0x144 */ char unk_144[0x194]; + /* 0x144 */ SkelAnime skelAnime; + /* 0x188 */ Vec3s jointTable[KEATON_LIMB_MAX]; + /* 0x206 */ Vec3s morphTable[KEATON_LIMB_MAX]; + /* 0x284 */ ColliderCylinder collider; + /* 0x2D0 */ s32 textBitSet; + /* 0x2D4 */ UNK_TYPE1 unk2D4[2]; + /* 0x2D6 */ s16 timer; /* 0x2D8 */ EnKitanActionFunc actionFunc; } EnKitan; // size = 0x2DC diff --git a/tools/disasm/functions.txt b/tools/disasm/functions.txt index b11b5f2983..a10a842f0b 100644 --- a/tools/disasm/functions.txt +++ b/tools/disasm/functions.txt @@ -16632,21 +16632,21 @@ 0x80C08FEC:("ObjMilkBin_Draw",), 0x80C090D0:("EnKitan_Init",), 0x80C09210:("EnKitan_Destroy",), - 0x80C0923C:("func_80C0923C",), - 0x80C09390:("func_80C09390",), - 0x80C09418:("func_80C09418",), - 0x80C094A8:("func_80C094A8",), - 0x80C09518:("func_80C09518",), - 0x80C095C8:("func_80C095C8",), - 0x80C09648:("func_80C09648",), - 0x80C09708:("func_80C09708",), - 0x80C09990:("func_80C09990",), - 0x80C09AA4:("func_80C09AA4",), - 0x80C09B50:("func_80C09B50",), + 0x80C0923C:("EnKitan_SpawnEffects",), + 0x80C09390:("EnKitan_CanTalk",), + 0x80C09418:("EnKitan_GetQuestionMessageId",), + 0x80C094A8:("EnKitan_Leave",), + 0x80C09518:("EnKitan_TalkAfterGivingPrize",), + 0x80C095C8:("EnKitan_WaitForPrizeTextboxClosed",), + 0x80C09648:("EnKitan_OfferPrize",), + 0x80C09708:("EnKitan_Talk",), + 0x80C09990:("EnKitan_WaitForPlayer",), + 0x80C09AA4:("EnKitan_Appear",), + 0x80C09B50:("EnKitan_WaitToAppear",), 0x80C09BC8:("EnKitan_Update",), - 0x80C09C74:("func_80C09C74",), - 0x80C09C90:("func_80C09C90",), - 0x80C09CD0:("func_80C09CD0",), + 0x80C09C74:("EnKitan_OverrideLimbDraw",), + 0x80C09C90:("EnKitan_PostLimbDraw",), + 0x80C09CD0:("EnKitan_Draw",), 0x80C09ED0:("func_80C09ED0",), 0x80C09FEC:("BgAstrBombwall_Init",), 0x80C0A0EC:("BgAstrBombwall_Destroy",),