/* * File: z_actor.c * Description: */ #include "fault.h" #include "attributes.h" #include "sys_cfb.h" #include "libu64/loadfragment.h" // Variables are put before most headers as a hacky way to bypass bss reordering FaultClient sActorFaultClient; // 2 funcs struct CollisionPoly* D_801ED8B0; // 1 func s32 D_801ED8B4; // 2 funcs struct Actor* sNearestAttentionActor; struct Actor* sPrioritizedAttentionActor; struct Actor* sNearestCameraDriftActor; struct Actor* sPrioritizedCameraDriftActor; f32 sNearestAttentionActorDistSq; f32 sBgmEnemyDistSq; f32 sNearestCameraDriftActorDistSq; s32 sHighestAttentionPriority; s32 sHighestCameraDriftPriority; s16 sAttentionPlayerRotY; Mtx sActorHiliteMtx; struct Actor* gCameraDriftActor; #include "z64actor.h" #include "z64door.h" #include "z64circle_tex.h" #include "z64horse.h" #include "zelda_arena.h" #include "z64quake.h" #include "z64rumble.h" #include "overlays/actors/ovl_En_Horse/z_en_horse.h" #include "overlays/actors/ovl_En_Part/z_en_part.h" #include "overlays/actors/ovl_En_Box/z_en_box.h" #include "assets/objects/gameplay_keep/gameplay_keep.h" #include "assets/objects/gameplay_dangeon_keep/gameplay_dangeon_keep.h" #include "assets/objects/object_bdoor/object_bdoor.h" #define ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_1 (1 << 0) #define ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_2 (1 << 1) // identical behavior to ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_1 #define ACTOR_AUDIO_FLAG_SFX_CENTERED_1 (1 << 2) #define ACTOR_AUDIO_FLAG_SFX_CENTERED_2 (1 << 3) // identical behavior to ACTOR_AUDIO_FLAG_SFX_CENTERED_1 #define ACTOR_AUDIO_FLAG_SFX_TIMER (1 << 4) #define ACTOR_AUDIO_FLAG_SEQ_KAMARO_DANCE (1 << 5) #define ACTOR_AUDIO_FLAG_SEQ_MUSIC_BOX_HOUSE (1 << 6) #define ACTOR_AUDIO_FLAG_SFX_ALL \ (ACTOR_AUDIO_FLAG_SFX_TIMER | ACTOR_AUDIO_FLAG_SFX_CENTERED_2 | ACTOR_AUDIO_FLAG_SFX_CENTERED_1 | \ ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_2 | ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_1) #define ACTOR_AUDIO_FLAG_SEQ_ALL (ACTOR_AUDIO_FLAG_SEQ_MUSIC_BOX_HOUSE | ACTOR_AUDIO_FLAG_SEQ_KAMARO_DANCE) #define ACTOR_AUDIO_FLAG_ALL (ACTOR_AUDIO_FLAG_SFX_ALL | ACTOR_AUDIO_FLAG_SEQ_ALL) void Actor_KillAllOnHalfDayChange(PlayState* play, ActorContext* actorCtx); Actor* Actor_SpawnEntry(ActorContext* actorCtx, ActorEntry* actorEntry, PlayState* play); Actor* Actor_Delete(ActorContext* actorCtx, Actor* actor, PlayState* play); void Attention_FindActor(PlayState* play, ActorContext* actorCtx, Actor** attentionActorP, Actor** cameraDriftActorP, Player* player); s32 Actor_CullingVolumeTest(PlayState* play, Actor* actor, Vec3f* projPos, f32 projW); void Actor_AddToCategory(ActorContext* actorCtx, Actor* actor, u8 actorCategory); Actor* Actor_RemoveFromCategory(PlayState* play, ActorContext* actorCtx, Actor* actorToRemove); void Actor_PrintLists(ActorContext* actorCtx) { ActorListEntry* actorList = &actorCtx->actorLists[0]; Actor* actor; s32 category; FaultDrawer_SetCharPad(-2, 0); FaultDrawer_Printf("actor\n", gMaxActorId); FaultDrawer_Printf("No. Actor Name Part SegName\n"); for (category = 0; category < ACTORCAT_MAX; category++) { actor = actorList[category].first; while (actor != NULL) { FaultDrawer_Printf("%3d %08x %04x %3d %s\n", category, actor, actor->id, actor->category, ""); actor = actor->next; } } } void ActorShape_Init(ActorShape* actorShape, f32 yOffset, ActorShadowFunc shadowDraw, f32 shadowScale) { actorShape->yOffset = yOffset; actorShape->shadowDraw = shadowDraw; actorShape->shadowScale = shadowScale; actorShape->shadowAlpha = 255; } void ActorShadow_Draw(Actor* actor, Lights* lights, PlayState* play, Gfx* dList, Color_RGBA8* color) { if (actor->floorPoly != NULL) { f32 dy = actor->world.pos.y - actor->floorHeight; if (dy >= -50.0f && dy < 500.0f) { f32 shadowScale; MtxF mtx; OPEN_DISPS(play->state.gfxCtx); POLY_OPA_DISP = Gfx_SetupDL(POLY_OPA_DISP, SETUPDL_44); gDPSetCombineLERP(POLY_OPA_DISP++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, COMBINED, 0, 0, 0, COMBINED); dy = CLAMP(dy, 0.0f, 150.0f); shadowScale = 1.0f - (dy * (1.0f / 350.0f)); if ((dy * (1.0f / 350.0f)) > 1.0f) { shadowScale = 0.0f; } if (color != NULL) { gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, color->r, color->g, color->b, (u8)(actor->shape.shadowAlpha * shadowScale)); } else { gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 0, 0, (u8)(actor->shape.shadowAlpha * shadowScale)); } func_800C0094(actor->floorPoly, actor->world.pos.x, actor->floorHeight, actor->world.pos.z, &mtx); Matrix_Put(&mtx); if ((dList != gCircleShadowDL) || (actor->scale.x != actor->scale.z)) { Matrix_RotateYS(actor->shape.rot.y, MTXMODE_APPLY); } shadowScale *= actor->shape.shadowScale; Matrix_Scale(actor->scale.x * shadowScale, 1.0f, actor->scale.z * shadowScale, MTXMODE_APPLY); MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_OPA_DISP++, dList); CLOSE_DISPS(play->state.gfxCtx); } } } void ActorShadow_DrawCircle(Actor* actor, Lights* lights, PlayState* play) { if (actor->bgCheckFlags & BGCHECKFLAG_PLAYER_400) { func_800B4AEC(play, actor, 50.0f); } ActorShadow_Draw(actor, lights, play, gCircleShadowDL, NULL); } void ActorShadow_DrawSquare(Actor* actor, Lights* lights, PlayState* play) { if (actor->bgCheckFlags & BGCHECKFLAG_PLAYER_400) { func_800B4AEC(play, actor, 50.0f); } ActorShadow_Draw(actor, lights, play, gSquareShadowDL, NULL); } void ActorShadow_DrawWhiteCircle(Actor* actor, Lights* lights, PlayState* play) { static Color_RGBA8 sColor = { 255, 255, 255, 255 }; ActorShadow_Draw(actor, lights, play, gCircleShadowDL, &sColor); } void ActorShadow_DrawHorse(Actor* actor, Lights* lights, PlayState* play) { ActorShadow_Draw(actor, lights, play, gHorseShadowDL, NULL); } void ActorShadow_DrawFoot(PlayState* play, Light* light, MtxF* arg2, s32 lightNum, f32 shadowAlpha, f32 shadowScaleX, f32 shadowScaleZ) { s32 pad; s16 sp58; f32 dir2; f32 dir0; OPEN_DISPS(play->state.gfxCtx); gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 0, 0, (u8)(CLAMP_MAX(lightNum * 1.3e-05f, 1.0f) * shadowAlpha)); dir0 = light->l.dir[0]; dir2 = light->l.dir[2]; sp58 = Math_Atan2S_XY(dir2, dir0); shadowScaleZ *= (4.5f - (light->l.dir[1] * 0.035f)); shadowScaleZ = CLAMP_MIN(shadowScaleZ, 1.0f); Matrix_Put(arg2); Matrix_RotateYS(sp58, MTXMODE_APPLY); Matrix_Scale(shadowScaleX, 1.0f, shadowScaleX * shadowScaleZ, MTXMODE_APPLY); MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_OPA_DISP++, gFootShadowDL); CLOSE_DISPS(play->state.gfxCtx); } void ActorShadow_DrawFeet(Actor* actor, Lights* mapper, PlayState* play) { f32 distToFloor = actor->world.pos.y - actor->floorHeight; if (distToFloor > 0.0f) { f32 shadowScale = actor->shape.shadowScale; u8 shadowAlpha = actor->shape.shadowAlpha; f32 alphaRatio; if ((actor->id == ACTOR_PLAYER) && (((Player*)actor)->stateFlags3 & PLAYER_STATE3_8000)) { f32 prevScaleZ = actor->scale.z; actor->scale.z += 0.03f * fabsf(Math_CosS(((Player*)actor)->unk_AAA)); actor->shape.shadowScale *= 0.2f; alphaRatio = distToFloor * 0.03f; actor->shape.shadowAlpha = actor->shape.shadowAlpha * CLAMP_MAX(alphaRatio, 1.0f); ActorShadow_Draw(actor, mapper, play, gCircleShadowDL, NULL); actor->scale.z = prevScaleZ; } else { actor->shape.shadowScale *= 0.3f; alphaRatio = (distToFloor - 20.0f) * 0.02f; actor->shape.shadowAlpha = actor->shape.shadowAlpha * CLAMP_MAX(alphaRatio, 1.0f); ActorShadow_DrawCircle(actor, mapper, play); } actor->shape.shadowScale = shadowScale; actor->shape.shadowAlpha = shadowAlpha; } if (distToFloor < 200.0f) { MtxF sp13C; MtxF spFC; CollisionPoly* poly; s32 bgId; f32 floorHeight[2]; Light* firstLight = &mapper->l.l[0]; f32 shadowAlpha; f32 shadowScaleX; f32 shadowScaleZ; Light* lightPtr; s32 lightNumMax; s32 i; s32 j; s32 lightNum; Vec3f* feetPosPtr; s32 numLights; f32* floorHeightPtr; s32 spB8; numLights = mapper->numLights - 2; feetPosPtr = actor->shape.feetPos; floorHeightPtr = floorHeight; OPEN_DISPS(play->state.gfxCtx); POLY_OPA_DISP = Gfx_SetupDL(POLY_OPA_DISP, SETUPDL_44); actor->shape.feetFloorFlags = 0; spB8 = 2; for (i = 0; i < ARRAY_COUNT(floorHeight); i++, spB8 >>= 1) { feetPosPtr->y += 50.0f; *floorHeightPtr = Play_GetFloorSurfaceImpl(play, &sp13C, &poly, &bgId, feetPosPtr); feetPosPtr->y -= 50.0f; distToFloor = feetPosPtr->y - *floorHeightPtr; if ((distToFloor >= -1.0f) && (distToFloor < 500.0f)) { lightNumMax = 0; if (distToFloor <= 10.0f) { actor->shape.feetFloorFlags |= spB8; if ((actor->depthInWater < 0.0f) && (bgId == BGCHECK_SCENE) && (actor->shape.unk_17 & spB8)) { if (SurfaceType_HasMaterialProperty(&play->colCtx, poly, bgId, MATERIAL_PROPERTY_SOFT_IMPRINT)) { SkinMatrix_MtxFCopy(&sp13C, &spFC); SkinMatrix_MulYRotation(&spFC, actor->shape.rot.y); EffFootmark_Add(play, &spFC, actor, i, feetPosPtr, (actor->shape.shadowScale * 0.3f), IREG(88) + 80, IREG(89) + 60, IREG(90) + 40, 30000, 200, 60); } actor->shape.unk_17 &= ~spB8; } } if (distToFloor > 30.0f) { distToFloor = 30.0f; } shadowAlpha = actor->shape.shadowAlpha * (1.0f - (distToFloor * (1 / 30.0f))); shadowScaleZ = 1.0f - (distToFloor * (1.0f / 70.0f)); shadowScaleX = actor->shape.shadowScale * shadowScaleZ * actor->scale.x; lightPtr = firstLight; for (j = 0; j < numLights; j++) { if (lightPtr->l.dir[1] > 0) { lightNum = (lightPtr->l.col[0] + lightPtr->l.col[1] + lightPtr->l.col[2]) * ABS_ALT(lightPtr->l.dir[1]); if (lightNum > 0) { lightNumMax += lightNum; ActorShadow_DrawFoot(play, lightPtr, &sp13C, lightNum, shadowAlpha, shadowScaleX, shadowScaleZ); } } lightPtr++; } for (j = 0; j < 2; j++) { if (lightPtr->l.dir[1] > 0) { lightNum = ((lightPtr->l.col[0] + lightPtr->l.col[1] + lightPtr->l.col[2]) * ABS_ALT(lightPtr->l.dir[1])) - (lightNumMax * 8); if (lightNum > 0) { ActorShadow_DrawFoot(play, lightPtr, &sp13C, lightNum, shadowAlpha, shadowScaleX, shadowScaleZ); } } lightPtr++; } } feetPosPtr++; floorHeightPtr++; } if (!(actor->bgCheckFlags & BGCHECKFLAG_GROUND)) { actor->shape.feetFloorFlags = 0; } else if (actor->shape.feetFloorFlags == 3) { f32 footDistY = actor->shape.feetPos[FOOT_LEFT].y - actor->shape.feetPos[FOOT_RIGHT].y; if ((floorHeight[0] + footDistY) < (floorHeight[1] - footDistY)) { actor->shape.feetFloorFlags = 2; } else { actor->shape.feetFloorFlags = 1; } } CLOSE_DISPS(play->state.gfxCtx); } } void Actor_SetFeetPos(Actor* actor, s32 limbIndex, s32 leftFootIndex, Vec3f* leftFootPos, s32 rightFootIndex, Vec3f* rightFootPos) { if (limbIndex == leftFootIndex) { Matrix_MultVec3f(leftFootPos, &actor->shape.feetPos[FOOT_LEFT]); } else if (limbIndex == rightFootIndex) { Matrix_MultVec3f(rightFootPos, &actor->shape.feetPos[FOOT_RIGHT]); } } void func_800B4AEC(PlayState* play, Actor* actor, f32 y) { s32 floorBgId; f32 yPos = actor->world.pos.y; actor->world.pos.y += y; actor->floorHeight = BgCheck_EntityRaycastFloor5_2(play, &play->colCtx, &actor->floorPoly, &floorBgId, actor, &actor->world.pos); actor->floorBgId = floorBgId; actor->world.pos.y = yPos; } void func_800B4B50(Actor* actor, Lights* mapper, PlayState* play) { f32 spEC; f32 temp_f12; f32 shadowScaleZ; f32 temp_f22; f32 temp_f24; f32 temp_f8; s32 lightNum; MtxF sp94; s32 numLights; s8 phi_v1; u8 temp_v0; Light* phi_s0; s32 lightNumMax; if (actor->bgCheckFlags & BGCHECKFLAG_PLAYER_400) { func_800B4AEC(play, actor, 50.0f); } if (actor->floorPoly != NULL) { s32 j; spEC = actor->world.pos.y - actor->floorHeight; if (spEC > 20.0f) { f32 temp_f20 = actor->shape.shadowScale; temp_v0 = actor->shape.shadowAlpha; actor->shape.shadowScale *= 0.3f; temp_f12 = (spEC - 20.0f) * 0.02f; actor->shape.shadowAlpha = CLAMP_MAX(temp_f12, 1.0f) * actor->shape.shadowAlpha; ActorShadow_DrawCircle(actor, mapper, play); actor->shape.shadowScale = temp_f20; actor->shape.shadowAlpha = temp_v0; } else if (spEC >= -1.0f) { numLights = mapper->numLights - 2; OPEN_DISPS(play->state.gfxCtx); POLY_OPA_DISP = Gfx_SetupDL(POLY_OPA_DISP, SETUPDL_44); func_800C0094(actor->floorPoly, actor->world.pos.x, actor->floorHeight, actor->world.pos.z, &sp94); temp_f22 = (f32)actor->shape.shadowAlpha * (1.0f - (spEC * (1.0f / 30.0f))); phi_s0 = mapper->l.l; shadowScaleZ = 1.0f - (spEC * (1.0f / 70.0f)); temp_f24 = actor->shape.shadowScale * shadowScaleZ * actor->scale.x; lightNumMax = 0; for (j = 0; j < numLights; j++, phi_s0++) { if (phi_s0->l.dir[1] > 0) { lightNum = (phi_s0->l.col[0] + phi_s0->l.col[1] + phi_s0->l.col[2]) * ABS_ALT(phi_s0->l.dir[1]); if (lightNum > 0) { lightNumMax += lightNum; ActorShadow_DrawFoot(play, phi_s0, &sp94, lightNum, temp_f22, temp_f24, shadowScaleZ); } } } for (j = 0; j < 2; j++, phi_s0++) { if (phi_s0->l.dir[1] > 0) { lightNum = ((phi_s0->l.col[0] + phi_s0->l.col[1] + phi_s0->l.col[2]) * ABS_ALT(phi_s0->l.dir[1])) - (8 * lightNumMax); if (lightNum > 0) { ActorShadow_DrawFoot(play, phi_s0, &sp94, lightNum, temp_f22, temp_f24, shadowScaleZ); } } } CLOSE_DISPS(play->state.gfxCtx); } } } void Actor_GetProjectedPos(PlayState* play, Vec3f* worldPos, Vec3f* projectedPos, f32* invW) { SkinMatrix_Vec3fMtxFMultXYZW(&play->viewProjectionMtxF, worldPos, projectedPos, invW); *invW = (*invW < 1.0f) ? 1.0f : (1.0f / *invW); } typedef struct AttentionColor { /* 0x0 */ Color_RGBA8 primary; // Used for Tatl's inner color, lock-on arrow, and lock-on reticle /* 0x4 */ Color_RGBA8 secondary; // Used for Tatl's outer color } AttentionColor; // size = 0x8 AttentionColor sAttentionColors[ACTORCAT_MAX + 1] = { { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_SWITCH { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_BG { { 255, 255, 230, 255 }, { 220, 160, 80, 0 } }, // ACTORCAT_PLAYER { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_EXPLOSIVES { { 150, 150, 255, 255 }, { 150, 150, 255, 0 } }, // ACTORCAT_NPC { { 255, 255, 0, 255 }, { 200, 155, 0, 0 } }, // ACTORCAT_ENEMY { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_PROP { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_ITEMACTION { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_MISC { { 255, 255, 0, 255 }, { 200, 155, 0, 0 } }, // ACTORCAT_BOSS { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_DOOR { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // ACTORCAT_CHEST { { 0, 255, 0, 255 }, { 0, 255, 0, 0 } }, // unused extra entry }; void Attention_SetReticlePos(Attention* attention, s32 reticleNum, f32 x, f32 y, f32 z) { attention->lockOnReticles[reticleNum].pos.x = x; attention->lockOnReticles[reticleNum].pos.y = y; attention->lockOnReticles[reticleNum].pos.z = z; attention->lockOnReticles[reticleNum].radius = attention->reticleRadius; } void Attention_InitReticle(Attention* attention, ActorType actorCategory, PlayState* play) { LockOnReticle* reticle; AttentionColor* attentionColor = &sAttentionColors[actorCategory]; s32 i; Math_Vec3f_Copy(&attention->reticlePos, &play->view.eye); attention->reticleRadius = 500.0f; // radius starts wide to zoom in on the actor attention->reticleFadeAlphaControl = 256; reticle = &attention->lockOnReticles[0]; for (i = 0; i < ARRAY_COUNT(attention->lockOnReticles); i++, reticle++) { Attention_SetReticlePos(attention, i, 0.0f, 0.0f, 0.0f); reticle->color.r = attentionColor->primary.r; reticle->color.g = attentionColor->primary.g; reticle->color.b = attentionColor->primary.b; } } void Attention_SetTatlState(Attention* attention, Actor* actor, ActorType actorCategory, PlayState* play) { AttentionColor* attentionColor = &sAttentionColors[actorCategory]; attention->tatlHoverPos.x = actor->focus.pos.x; attention->tatlHoverPos.y = actor->focus.pos.y + (actor->lockOnArrowOffset * actor->scale.y); attention->tatlHoverPos.z = actor->focus.pos.z; attention->tatlInnerColor.r = attentionColor->primary.r; attention->tatlInnerColor.g = attentionColor->primary.g; attention->tatlInnerColor.b = attentionColor->primary.b; attention->tatlInnerColor.a = attentionColor->primary.a; attention->tatlOuterColor.r = attentionColor->secondary.r; attention->tatlOuterColor.g = attentionColor->secondary.g; attention->tatlOuterColor.b = attentionColor->secondary.b; attention->tatlOuterColor.a = attentionColor->secondary.a; } void Attention_Init(Attention* attention, Actor* actor, PlayState* play) { attention->tatlHoverActor = attention->reticleActor = attention->forcedLockOnActor = attention->bgmEnemy = NULL; attention->reticleSpinCounter = 0; attention->curReticle = 0; attention->tatlMoveProgressFactor = 0.0f; Attention_SetTatlState(attention, actor, actor->category, play); Attention_InitReticle(attention, actor->category, play); } void Attention_Draw(Attention* attention, PlayState* play) { Player* player = GET_PLAYER(play); Actor* actor; // used for both the reticle actor and arrow hover actor if (player->stateFlags1 & (PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_400 | PLAYER_STATE1_10000000 | PLAYER_STATE1_20000000)) { return; } actor = attention->reticleActor; OPEN_DISPS(play->state.gfxCtx); if (attention->reticleFadeAlphaControl != 0) { LockOnReticle* reticle; s16 alpha = 255; f32 projectedPosScale = 1.0f; Vec3f projectedPos; s32 numReticles; f32 invW; s32 i; s32 curReticle; f32 lockOnScaleX; if (attention->reticleSpinCounter != 0) { // Reticle is spinning so it is active, only need to draw one numReticles = 1; } else { // Use multiple reticles for the motion blur effect from the reticle // quickly zooming in on an actor from off screen numReticles = ARRAY_COUNT(attention->lockOnReticles); } if (actor != NULL) { Math_Vec3f_Copy(&attention->reticlePos, &actor->focus.pos); projectedPosScale = (500.0f - attention->reticleRadius) / 420.0f; } else { // Not locked on, start fading out attention->reticleFadeAlphaControl -= 120; if (attention->reticleFadeAlphaControl < 0) { attention->reticleFadeAlphaControl = 0; } // `reticleFadeAlphaControl` is only used as an alpha when fading out. // Otherwise it defaults to 255, set above. alpha = attention->reticleFadeAlphaControl; } Actor_GetProjectedPos(play, &attention->reticlePos, &projectedPos, &invW); projectedPos.x = ((SCREEN_WIDTH / 2) * (projectedPos.x * invW)) * projectedPosScale; projectedPos.x = CLAMP(projectedPos.x, -SCREEN_WIDTH, SCREEN_WIDTH); projectedPos.y = ((SCREEN_HEIGHT / 2) * (projectedPos.y * invW)) * projectedPosScale; projectedPos.y = CLAMP(projectedPos.y, -SCREEN_HEIGHT, SCREEN_HEIGHT); projectedPos.z *= projectedPosScale; attention->curReticle--; if (attention->curReticle < 0) { attention->curReticle = ARRAY_COUNT(attention->lockOnReticles) - 1; } Attention_SetReticlePos(attention, attention->curReticle, projectedPos.x, projectedPos.y, projectedPos.z); if (!(player->stateFlags1 & PLAYER_STATE1_TALKING) || (actor != player->focusActor)) { OVERLAY_DISP = Gfx_SetupDL(OVERLAY_DISP, SETUPDL_57); for (i = 0, curReticle = attention->curReticle; i < numReticles; i++, curReticle = (curReticle + 1) % ARRAY_COUNT(attention->lockOnReticles)) { reticle = &attention->lockOnReticles[curReticle]; if (reticle->radius < 500.0f) { s32 triangleIndex; if (reticle->radius <= 120.0f) { lockOnScaleX = 0.15f; } else { lockOnScaleX = ((reticle->radius - 120.0f) * 0.001f) + 0.15f; } Matrix_Translate(reticle->pos.x, reticle->pos.y, 0.0f, MTXMODE_NEW); Matrix_Scale(lockOnScaleX, 0.15f, 1.0f, MTXMODE_APPLY); gDPSetPrimColor(OVERLAY_DISP++, 0, 0, reticle->color.r, reticle->color.g, reticle->color.b, (u8)alpha); Matrix_RotateZS(attention->reticleSpinCounter * 0x200, MTXMODE_APPLY); // Draw the 4 triangles that make up the reticle for (triangleIndex = 0; triangleIndex < 4; triangleIndex++) { Matrix_RotateZS(0x10000 / 4, MTXMODE_APPLY); Matrix_Push(); Matrix_Translate(reticle->radius, reticle->radius, 0.0f, MTXMODE_APPLY); MATRIX_FINALIZE_AND_LOAD(OVERLAY_DISP++, play->state.gfxCtx); gSPDisplayList(OVERLAY_DISP++, gLockOnReticleTriangleDL); Matrix_Pop(); } } alpha -= 255 / ARRAY_COUNT(attention->lockOnReticles); if (alpha < 0) { alpha = 0; } } } } actor = attention->arrowHoverActor; if ((actor != NULL) && !(actor->flags & ACTOR_FLAG_LOCK_ON_DISABLED)) { AttentionColor* attentionColor = &sAttentionColors[actor->category]; POLY_XLU_DISP = Gfx_SetupDL(POLY_XLU_DISP, SETUPDL_7); Matrix_Translate(actor->focus.pos.x, actor->focus.pos.y + (actor->lockOnArrowOffset * actor->scale.y) + 17.0f, actor->focus.pos.z, MTXMODE_NEW); Matrix_RotateYS(play->gameplayFrames * 0xBB8, MTXMODE_APPLY); Matrix_Scale((iREG(27) + 35) / 1000.0f, (iREG(28) + 60) / 1000.0f, (iREG(29) + 50) / 1000.0f, MTXMODE_APPLY); gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, attentionColor->primary.r, attentionColor->primary.g, attentionColor->primary.b, 255); MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_XLU_DISP++, gLockOnArrowDL); } CLOSE_DISPS(play->state.gfxCtx); } void Attention_Update(Attention* attention, Player* player, Actor* playerFocusActor, PlayState* play) { s32 pad; Actor* actor; // used for both the Tatl hover actor and reticle actor s32 category; Vec3f projectedPos; f32 invW; actor = NULL; if ((player->focusActor != NULL) && (player->controlStickDirections[player->controlStickDataIndex] == PLAYER_STICK_DIR_BACKWARD)) { // Holding backward on the control stick prevents an arrow appearing over the next lock-on actor. // This helps escape a lock-on loop when using Switch Targeting, but note that this still works for // Hold Targeting as well. attention->arrowHoverActor = NULL; } else { // Find the next attention actor so Tatl and an arrow can hover over it (if applicable) Attention_FindActor(play, &play->actorCtx, &actor, &gCameraDriftActor, player); attention->arrowHoverActor = actor; } if (attention->forcedLockOnActor != NULL) { // This lock-on actor takes precedence over anything else // (this feature is never used in practice) actor = attention->forcedLockOnActor; attention->forcedLockOnActor = NULL; } else if (playerFocusActor != NULL) { // Stay locked-on to the same actor, if there is one. // This also makes Tatl fly over to the current focus actor, if there is one. actor = playerFocusActor; } if (actor != NULL) { category = actor->category; } else { category = player->actor.category; } if ((actor != attention->tatlHoverActor) || (category != attention->tatlHoverActorCategory)) { // Set Tatl to hover over a new actor attention->tatlHoverActor = actor; attention->tatlHoverActorCategory = category; attention->tatlMoveProgressFactor = 1.0f; } if (actor == NULL) { // Setting the actor to Player will make Tatl return to him actor = &player->actor; } if (!Math_StepToF(&attention->tatlMoveProgressFactor, 0.0f, 0.25f)) { f32 moveScale = 0.25f / attention->tatlMoveProgressFactor; f32 x = actor->focus.pos.x - attention->tatlHoverPos.x; f32 y = (actor->focus.pos.y + (actor->lockOnArrowOffset * actor->scale.y)) - attention->tatlHoverPos.y; f32 z = actor->focus.pos.z - attention->tatlHoverPos.z; attention->tatlHoverPos.x += x * moveScale; attention->tatlHoverPos.y += y * moveScale; attention->tatlHoverPos.z += z * moveScale; } else { // Set Tatl pos and color after reaching destination Attention_SetTatlState(attention, actor, category, play); } if ((playerFocusActor != NULL) && (attention->reticleSpinCounter == 0)) { Actor_GetProjectedPos(play, &playerFocusActor->focus.pos, &projectedPos, &invW); if ((projectedPos.z <= 0.0f) || (fabsf(projectedPos.x * invW) >= 1.0f) || (fabsf(projectedPos.y * invW) >= 1.0f)) { // Release the reticle if the actor is off screen. // It is possible to move far enough away from an actor that it goes off screen, despite being // locked onto it. In this case the reticle will release, but the lock-on will remain // because Player is still updating focusActor. // It is unclear if this is intentional, or if it is a bug and the lock-on as a whole is supposed // to release. playerFocusActor = NULL; } } if (playerFocusActor != NULL) { if (playerFocusActor != attention->reticleActor) { s32 lockOnSfxId; // Set up a new reticle Attention_InitReticle(attention, playerFocusActor->category, play); attention->reticleActor = playerFocusActor; if (playerFocusActor->id == ACTOR_EN_BOOM) { // Don't draw the reticle when locked onto the zora fin boomerang. // Note that it isn't possible to lock onto the boomerang, so this code doesn't do anything. // This implies that the boomerang camera lock may have been implemented with Z-Targeting at one point, // but was eventually implemented as its own camera mode instead. attention->reticleFadeAlphaControl = 0; } lockOnSfxId = CHECK_FLAG_ALL(playerFocusActor->flags, ACTOR_FLAG_ATTENTION_ENABLED | ACTOR_FLAG_HOSTILE) ? NA_SE_SY_LOCK_ON : NA_SE_SY_LOCK_ON_HUMAN; Audio_PlaySfx(lockOnSfxId); } // Update reticle attention->reticlePos.x = playerFocusActor->world.pos.x; attention->reticlePos.y = playerFocusActor->world.pos.y - (playerFocusActor->shape.yOffset * playerFocusActor->scale.y); attention->reticlePos.z = playerFocusActor->world.pos.z; if (attention->reticleSpinCounter == 0) { f32 step = (500.0f - attention->reticleRadius) * 3.0f; f32 reticleZoomStep = CLAMP(step, 30.0f, 100.0f); if (Math_StepToF(&attention->reticleRadius, 80.0f, reticleZoomStep)) { // Non-zero counter indicates the reticle is done zooming in attention->reticleSpinCounter++; } } else { // Finished zooming in, spin the reticle around the lock-on actor // 0x80 is or'd to avoid a value of zero. // This rotation value gets multiplied by 0x200, which multiplied by 0x80 gives a full turn (0x10000) attention->reticleSpinCounter = (attention->reticleSpinCounter + 3) | 0x80; attention->reticleRadius = 120.0f; } } else { // Expand the radius quickly as the reticle is released attention->reticleActor = NULL; Math_StepToF(&attention->reticleRadius, 500.0f, 80.0f); } } /* Start of Flags section */ /** * Tests if a current scene switch flag is set. */ s32 Flags_GetSwitch(PlayState* play, s32 flag) { if ((flag > SWITCH_FLAG_NONE) && (flag < 0x80)) { return play->actorCtx.sceneFlags.switches[(flag & ~0x1F) >> 5] & (1 << (flag & 0x1F)); } return 0; } /** * Sets a current scene switch flag. */ void Flags_SetSwitch(PlayState* play, s32 flag) { if ((flag > SWITCH_FLAG_NONE) && (flag < 0x80)) { play->actorCtx.sceneFlags.switches[(flag & ~0x1F) >> 5] |= 1 << (flag & 0x1F); } } /** * Unsets a current scene switch flag. */ void Flags_UnsetSwitch(PlayState* play, s32 flag) { if ((flag > SWITCH_FLAG_NONE) && (flag < 0x80)) { play->actorCtx.sceneFlags.switches[(flag & ~0x1F) >> 5] &= ~(1 << (flag & 0x1F)); } } /** * Tests if a current scene chest flag is set. */ s32 Flags_GetTreasure(PlayState* play, s32 flag) { return play->actorCtx.sceneFlags.chest & (1 << flag); } /** * Sets a current scene chest flag. */ void Flags_SetTreasure(PlayState* play, s32 flag) { play->actorCtx.sceneFlags.chest |= (1 << flag); } /** * Overrides all the current scene chest flags. */ void Flags_SetAllTreasure(PlayState* play, s32 flag) { play->actorCtx.sceneFlags.chest = flag; } /** * Returns all the current scene chest flags. */ s32 Flags_GetAllTreasure(PlayState* play) { return play->actorCtx.sceneFlags.chest; } /** * Tests if a current scene clear flag is set. */ s32 Flags_GetClear(PlayState* play, s32 roomNumber) { return play->actorCtx.sceneFlags.clearedRoom & (1 << roomNumber); } /** * Sets a current scene clear flag. */ void Flags_SetClear(PlayState* play, s32 roomNumber) { play->actorCtx.sceneFlags.clearedRoom |= (1 << roomNumber); } /** * Unsets a current scene clear flag. */ void Flags_UnsetClear(PlayState* play, s32 roomNumber) { play->actorCtx.sceneFlags.clearedRoom &= ~(1 << roomNumber); } /** * Tests if a current scene temp clear flag is set. */ s32 Flags_GetClearTemp(PlayState* play, s32 roomNumber) { return play->actorCtx.sceneFlags.clearedRoomTemp & (1 << roomNumber); } /** * Sets a current scene temp clear flag. */ void Flags_SetClearTemp(PlayState* play, s32 roomNumber) { play->actorCtx.sceneFlags.clearedRoomTemp |= (1 << roomNumber); } /** * Unsets a current scene temp clear flag. */ void Flags_UnsetClearTemp(PlayState* play, s32 roomNumber) { play->actorCtx.sceneFlags.clearedRoomTemp &= ~(1 << roomNumber); } /** * Tests if a current scene collectible flag is set. */ s32 Flags_GetCollectible(PlayState* play, s32 flag) { if ((flag > 0) && (flag < 0x80)) { return play->actorCtx.sceneFlags.collectible[(flag & ~0x1F) >> 5] & (1 << (flag & 0x1F)); } return 0; } /** * Sets a current scene collectible flag. */ void Flags_SetCollectible(PlayState* play, s32 flag) { if ((flag > 0) && (flag < 0x80)) { play->actorCtx.sceneFlags.collectible[(flag & ~0x1F) >> 5] |= 1 << (flag & 0x1F); } } /* End of Flags section */ /* Start of TitleCard section */ void TitleCard_ContextInit(GameState* gameState, TitleCardContext* titleCtx) { titleCtx->durationTimer = 0; titleCtx->delayTimer = 0; titleCtx->intensity = 0; titleCtx->alpha = 0; } void TitleCard_InitBossName(GameState* gameState, TitleCardContext* titleCtx, TexturePtr texture, s16 x, s16 y, u8 width, u8 height) { titleCtx->texture = texture; titleCtx->x = x; titleCtx->y = y; titleCtx->width = width; titleCtx->height = height; titleCtx->durationTimer = 80; titleCtx->delayTimer = 0; } void TitleCard_InitPlaceName(GameState* gameState, TitleCardContext* titleCtx, TexturePtr texture, s32 x, s32 y, s32 width, s32 height, s32 delay) { } void TitleCard_Update(GameState* gameState, TitleCardContext* titleCtx) { if (DECR(titleCtx->delayTimer) == 0) { if (DECR(titleCtx->durationTimer) == 0) { Math_StepToS(&titleCtx->alpha, 0, 30); Math_StepToS(&titleCtx->intensity, 0, 70); } else { Math_StepToS(&titleCtx->alpha, 255, 10); Math_StepToS(&titleCtx->intensity, 255, 20); } } } void TitleCard_Draw(GameState* gameState, TitleCardContext* titleCtx) { if (titleCtx->alpha != 0) { s32 width = titleCtx->width; s32 height = titleCtx->height; s32 doubleWidth = width * 2; s32 titleX = (titleCtx->x * 4) - doubleWidth; s32 doubleHeight = height * 2; s32 titleY = (titleCtx->y * 4) - doubleHeight; s32 titleSecondY; s32 textureLanguageOffset; OPEN_DISPS(gameState->gfxCtx); if (width * height > TMEM_SIZE) { height = TMEM_SIZE / width; } titleSecondY = titleY + (height * 4); OVERLAY_DISP = Gfx_SetupDL52_NoCD(OVERLAY_DISP); gDPSetPrimColor(OVERLAY_DISP++, 0, 0, (u8)titleCtx->intensity, (u8)titleCtx->intensity, (u8)titleCtx->intensity, (u8)titleCtx->alpha); gDPLoadTextureBlock(OVERLAY_DISP++, titleCtx->texture, G_IM_FMT_IA, G_IM_SIZ_8b, width, height, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); gSPTextureRectangle(OVERLAY_DISP++, titleX, titleY, ((doubleWidth * 2) + titleX) - 4, titleY + (height * 4) - 1, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); height = titleCtx->height - height; // If texture is bigger than 0x1000, display the rest if (height > 0) { gDPLoadTextureBlock(OVERLAY_DISP++, (uintptr_t)titleCtx->texture + 0x1000, G_IM_FMT_IA, G_IM_SIZ_8b, width, height, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); gSPTextureRectangle(OVERLAY_DISP++, titleX, titleSecondY, ((doubleWidth * 2) + titleX) - 4, titleSecondY + (height * 4) - 1, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); } CLOSE_DISPS(gameState->gfxCtx); } } /* End of TitleCard section */ // unused s32 func_800B6434(PlayState* play, TitleCardContext* titleCtx) { if ((play->actorCtx.titleCtx.delayTimer != 0) || (play->actorCtx.titleCtx.alpha != 0)) { titleCtx->durationTimer = 0; titleCtx->delayTimer = 0; return false; } return true; } void Actor_InitPlayerImpact(PlayState* play) { play->actorCtx.playerImpact.timer = 0; } void Actor_UpdatePlayerImpact(PlayState* play) { DECR(play->actorCtx.playerImpact.timer); } s32 Actor_SetPlayerImpact(PlayState* play, PlayerImpactType type, s32 timer, f32 dist, Vec3f* pos) { if ((play->actorCtx.playerImpact.timer != 0) && (dist < play->actorCtx.playerImpact.dist)) { return false; } play->actorCtx.playerImpact.type = type; play->actorCtx.playerImpact.timer = timer; play->actorCtx.playerImpact.dist = dist; Math_Vec3f_Copy(&play->actorCtx.playerImpact.pos, pos); return true; } f32 Actor_GetPlayerImpact(PlayState* play, f32 range, Vec3f* pos, PlayerImpactType* type) { f32 dist; if ((play->actorCtx.playerImpact.timer == 0) || (range == 0.0f)) { return -1.0f; } dist = Math_Vec3f_DistXYZ(&play->actorCtx.playerImpact.pos, pos) / range; *type = play->actorCtx.playerImpact.type; return play->actorCtx.playerImpact.dist - dist; } /** * Initializes an element of the `play->actorCtx.actorSharedMemory` array to the `ptr` pointer, or allocates one using * the `size` argument in case `ptr` is NULL. This element is associated to an `id`. * * This allows allows different actors the ability to access the varible, and thus communicate with each other by * reading/setting the value. * * In success: returns the allocated pointer if `ptr` was NULL or the `ptr` pointer otherwise. * In failure (There's no space left in `play->actorCtx.actorSharedMemory` or an allocation error happened): returns * NULL. * * Note there are no duplicated id checks. * * Used only by EnLiftNuts. */ void* Actor_AddSharedMemoryEntry(PlayState* play, s16 id, void* ptr, size_t size) { ActorSharedMemoryEntry* entry = play->actorCtx.actorSharedMemory; s32 i; for (i = 0; i < ARRAY_COUNT(play->actorCtx.actorSharedMemory); i++) { if (entry->id == 0) { if (ptr == NULL) { ptr = ZeldaArena_Malloc(size); if (ptr == NULL) { return NULL; } entry->isDynamicallyInitialised = true; } entry->id = id; entry->ptr = ptr; return ptr; } entry++; } return NULL; } /** * Frees the first element of `play->actorCtx.actorSharedMemory` with id `id`. * * If success, the free'd pointer is returned. * If failure, NULL is returned. * * Used only by EnLiftNuts. */ void* Actor_FreeSharedMemoryEntry(PlayState* play, s16 id) { ActorSharedMemoryEntry* entry = play->actorCtx.actorSharedMemory; s32 i; for (i = 0; i < ARRAY_COUNT(play->actorCtx.actorSharedMemory); i++) { if (id == entry->id) { entry->id = 0; if (entry->isDynamicallyInitialised) { ZeldaArena_Free(entry->ptr); entry->isDynamicallyInitialised = false; } return entry->ptr; } entry++; } return NULL; } /** * Retrieves the first pointer stored with the id `id` from `play->actorCtx.actorSharedMemory`. * If there's no pointer stored with that id, NULL is returned. * * Used only by EnGamelupy. */ void* Actor_FindSharedMemoryEntry(PlayState* play, s16 id) { ActorSharedMemoryEntry* entry = play->actorCtx.actorSharedMemory; s32 i; for (i = 0; i < ARRAY_COUNT(play->actorCtx.actorSharedMemory); i++) { if (id == entry->id) { return entry->ptr; } entry++; } return NULL; } void Actor_Kill(Actor* actor) { actor->draw = NULL; actor->update = NULL; actor->flags &= ~ACTOR_FLAG_ATTENTION_ENABLED; } void Actor_SetWorldToHome(Actor* actor) { actor->world = actor->home; } void Actor_SetFocus(Actor* actor, f32 height) { actor->focus.pos.x = actor->world.pos.x; actor->focus.pos.y = actor->world.pos.y + height; actor->focus.pos.z = actor->world.pos.z; actor->focus.rot.x = actor->world.rot.x; actor->focus.rot.y = actor->world.rot.y; actor->focus.rot.z = actor->world.rot.z; } void Actor_SetWorldRotToShape(Actor* actor) { actor->world.rot = actor->shape.rot; } void Actor_SetShapeRotToWorld(Actor* actor) { actor->shape.rot = actor->world.rot; } void Actor_SetScale(Actor* actor, f32 scale) { actor->scale.z = scale; actor->scale.y = scale; actor->scale.x = scale; } void Actor_SetObjectDependency(PlayState* play, Actor* actor) { gSegments[0x06] = OS_K0_TO_PHYSICAL(play->objectCtx.slots[actor->objectSlot].segment); } void Actor_Init(Actor* actor, PlayState* play) { Actor_SetWorldToHome(actor); Actor_SetShapeRotToWorld(actor); Actor_SetFocus(actor, 0.0f); Math_Vec3f_Copy(&actor->prevPos, &actor->world.pos); Actor_SetScale(actor, 0.01f); actor->attentionRangeType = ATTENTION_RANGE_3; actor->terminalVelocity = -20.0f; actor->xyzDistToPlayerSq = FLT_MAX; actor->cullingVolumeDistance = 1000.0f; actor->cullingVolumeScale = 350.0f; actor->cullingVolumeDownward = 700.0f; actor->hintId = TATL_HINT_ID_NONE; CollisionCheck_InitInfo(&actor->colChkInfo); actor->floorBgId = BGCHECK_SCENE; ActorShape_Init(&actor->shape, 0.0f, NULL, 0.0f); if (Object_IsLoaded(&play->objectCtx, actor->objectSlot)) { Actor_SetObjectDependency(play, actor); actor->init(actor, play); actor->init = NULL; } } void Actor_Destroy(Actor* actor, PlayState* play) { if (actor->init == NULL) { if (actor->destroy != NULL) { actor->destroy(actor, play); actor->destroy = NULL; } } } f32 sActorMovementScale = 1.0f; void Actor_SetMovementScale(s32 scale) { sActorMovementScale = scale * 0.5f; } /** * Update actor position using velocity and any push from z_collision_check. */ void Actor_UpdatePos(Actor* actor) { f32 speedRate = sActorMovementScale; actor->world.pos.x += (actor->velocity.x * speedRate) + actor->colChkInfo.displacement.x; actor->world.pos.y += (actor->velocity.y * speedRate) + actor->colChkInfo.displacement.y; actor->world.pos.z += (actor->velocity.z * speedRate) + actor->colChkInfo.displacement.z; } /** * Updates actor's velocity accounting for gravity (without exceeding terminal velocity) * The operation is performed in cylindrical coordinates * * It is recommended to not call this function directly and use `Actor_MoveWithGravity` instead */ void Actor_UpdateVelocityWithGravity(Actor* actor) { actor->velocity.x = actor->speed * Math_SinS(actor->world.rot.y); actor->velocity.z = actor->speed * Math_CosS(actor->world.rot.y); actor->velocity.y += actor->gravity; if (actor->velocity.y < actor->terminalVelocity) { actor->velocity.y = actor->terminalVelocity; } } /** * Moves actor accounting for its current velocity and applying gravity * The operation is performed in cylindrical coordinates */ void Actor_MoveWithGravity(Actor* actor) { Actor_UpdateVelocityWithGravity(actor); Actor_UpdatePos(actor); } /** * Updates actor's velocity, ignoring gravity * The operation is performed in spherical coordinates * * It is recommended to not call this function directly and use `Actor_MoveWithoutGravity` instead */ void Actor_UpdateVelocityWithoutGravity(Actor* actor) { f32 speedXZ = Math_CosS(actor->world.rot.x) * actor->speed; actor->velocity.x = Math_SinS(actor->world.rot.y) * speedXZ; actor->velocity.y = Math_SinS(actor->world.rot.x) * actor->speed; actor->velocity.z = Math_CosS(actor->world.rot.y) * speedXZ; } /** * Moves actor accounting for its current velocity, without applying gravity * The operation is performed in spherical coordinates * * Useful for flying or swimming actors */ void Actor_MoveWithoutGravity(Actor* actor) { Actor_UpdateVelocityWithoutGravity(actor); Actor_UpdatePos(actor); } /** * Like `Actor_UpdateVelocityWithoutGravity`, but the actor is moved backwards instead of forwards * * It is recommended to not call this function directly and use `Actor_MoveWithoutGravityReverse` instead */ void Actor_UpdateVelocityWithoutGravityReverse(Actor* actor) { f32 speedXZ = Math_CosS(-actor->world.rot.x) * actor->speed; actor->velocity.x = Math_SinS(actor->world.rot.y) * speedXZ; actor->velocity.y = Math_SinS(-actor->world.rot.x) * actor->speed; actor->velocity.z = Math_CosS(actor->world.rot.y) * speedXZ; } /** * Like `Actor_MoveWithoutGravity`, but the actor is moved backwards instead of forwards */ void Actor_MoveWithoutGravityReverse(Actor* actor) { Actor_UpdateVelocityWithoutGravityReverse(actor); Actor_UpdatePos(actor); } /** * Sets horizontal speed and Y velocity using the `speed` argument and current pitch */ void Actor_SetSpeeds(Actor* actor, f32 speed) { actor->speed = Math_CosS(actor->world.rot.x) * speed; actor->velocity.y = -Math_SinS(actor->world.rot.x) * speed; } // unused void Actor_UpdatePosFromSkelAnime(Actor* actor, SkelAnime* skelAnime) { Vec3f pos; SkelAnime_UpdateTranslation(skelAnime, &pos, actor->shape.rot.y); actor->world.pos.x += pos.x * actor->scale.x; actor->world.pos.y += pos.y * actor->scale.y; actor->world.pos.z += pos.z * actor->scale.z; } s16 Actor_WorldYawTowardActor(Actor* actorA, Actor* actorB) { return Math_Vec3f_Yaw(&actorA->world.pos, &actorB->world.pos); } s16 Actor_FocusYawTowardActor(Actor* actorA, Actor* actorB) { return Math_Vec3f_Yaw(&actorA->focus.pos, &actorB->focus.pos); } s16 Actor_WorldYawTowardPoint(Actor* actor, Vec3f* refPoint) { return Math_Vec3f_Yaw(&actor->world.pos, refPoint); } s16 Actor_WorldPitchTowardActor(Actor* actorA, Actor* actorB) { return Math_Vec3f_Pitch(&actorA->world.pos, &actorB->world.pos); } s16 Actor_FocusPitchTowardActor(Actor* actorA, Actor* actorB) { return Math_Vec3f_Pitch(&actorA->focus.pos, &actorB->focus.pos); } s16 Actor_WorldPitchTowardPoint(Actor* actor, Vec3f* refPoint) { return Math_Vec3f_Pitch(&actor->world.pos, refPoint); } f32 Actor_WorldDistXYZToActor(Actor* actorA, Actor* actorB) { return Math_Vec3f_DistXYZ(&actorA->world.pos, &actorB->world.pos); } f32 Actor_WorldDistXYZToPoint(Actor* actor, Vec3f* refPoint) { return Math_Vec3f_DistXYZ(&actor->world.pos, refPoint); } f32 Actor_WorldDistXZToActor(Actor* actorA, Actor* actorB) { return Math_Vec3f_DistXZ(&actorA->world.pos, &actorB->world.pos); } f32 Actor_WorldDistXZToPoint(Actor* actor, Vec3f* refPoint) { return Math_Vec3f_DistXZ(&actor->world.pos, refPoint); } /** * Find the offset of a point from an actor in that actor's own coordinates (origin at the actor's * world.pos, z-axis is facing angle, i.e. shape.rot.y) * * @param[in] actor The actor whose coordinate system to transform to. * @param[out] offset The transformed coordinates. * @param[in] point The point to transform to actor coordinates. */ void Actor_WorldToActorCoords(Actor* actor, Vec3f* offset, Vec3f* point) { f32 cos = Math_CosS(actor->shape.rot.y); f32 sin = Math_SinS(actor->shape.rot.y); f32 diffX; f32 diffZ; // Shift X,Z to actor coordinates origin diffX = point->x - actor->world.pos.x; diffZ = point->z - actor->world.pos.z; // Rotate X and Z offsets to align Z to actor's shape.rot.y offset->x = ((diffX * cos) - (diffZ * sin)); offset->z = ((diffZ * cos) + (diffX * sin)); // Shift Y to origin offset->y = point->y - actor->world.pos.y; } f32 Actor_HeightDiff(Actor* actor1, Actor* actor2) { return actor2->world.pos.y - actor1->world.pos.y; } /** * Calculates and sets the control stick x/y values and writes these to input. */ void Actor_SetControlStickData(PlayState* play, Input* input, f32 controlStickMagnitude, s16 controlStickAngle) { s16 relativeAngle = controlStickAngle - Camera_GetInputDirYaw(GET_ACTIVE_CAM(play)); input->cur.stick_x = -Math_SinS(relativeAngle) * controlStickMagnitude; input->rel.stick_x = input->cur.stick_x; input->cur.stick_y = Math_CosS(relativeAngle) * controlStickMagnitude; input->rel.stick_y = input->cur.stick_y; } f32 Player_GetHeight(Player* player) { f32 extraHeight; if (player->stateFlags1 & PLAYER_STATE1_800000) { extraHeight = 32.0f; } else { extraHeight = 0.0f; } switch (player->transformation) { default: case PLAYER_FORM_FIERCE_DEITY: return extraHeight + 124.0f; case PLAYER_FORM_GORON: return extraHeight + ((player->stateFlags3 & PLAYER_STATE3_1000) ? 34.0f : 80.0f); case PLAYER_FORM_ZORA: return extraHeight + 68.0f; case PLAYER_FORM_DEKU: return extraHeight + 36.0f; case PLAYER_FORM_HUMAN: return extraHeight + 44.0f; } } f32 Player_GetRunSpeedLimit(Player* player) { if (player->stateFlags1 & PLAYER_STATE1_800000) { return 15.0f; } else if (player->stateFlags1 & PLAYER_STATE1_8000000) { return (R_RUN_SPEED_LIMIT / 100.0f) * 0.6f; } else { return R_RUN_SPEED_LIMIT / 100.0f; } } bool func_800B7118(Player* player) { return player->stateFlags1 & PLAYER_STATE1_8; } bool func_800B7128(Player* player) { return func_800B7118(player) && (player->unk_ACC != 0); } bool func_800B715C(PlayState* play) { Player* player = GET_PLAYER(play); return player->stateFlags2 & PLAYER_STATE2_8; } void Player_SetCameraHorseSetting(PlayState* play, Player* player) { if ((play->roomCtx.curRoom.type != ROOM_TYPE_4) && (player->actor.id == ACTOR_PLAYER)) { EnHorse* rideActor = (EnHorse*)player->rideActor; if ((rideActor != NULL) && !(rideActor->unk_1EC & 0x10)) { Camera_ChangeSetting(Play_GetCamera(play, CAM_ID_MAIN), CAM_SET_HORSE); } } } void Player_MountHorse(PlayState* play, Player* player, Actor* horse) { player->rideActor = horse; player->stateFlags1 |= PLAYER_STATE1_800000; horse->child = &player->actor; } bool func_800B7200(Player* player) { return (player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_20000000)) || (player->csAction != PLAYER_CSACTION_NONE); } void Player_SpawnHorse(PlayState* play, Player* player) { Horse_Spawn(play, player); } /** * Sets a Player Cutscene Action specified by `csAction`. * * `haltActorsDuringCsAction` being set to false in this function means that all actors will * be able to update while Player is performing the cutscene action. * * Note: due to how player implements initializing the cutscene action state, `haltActorsDuringCsAction` * will only be considered the first time player starts a `csAction`. * Player must leave the cutscene action state and enter it again before halting actors can be toggled. */ s32 Player_SetCsAction(PlayState* play, Actor* csActor, u8 csAction) { Player* player = GET_PLAYER(play); if ((player->csAction == PLAYER_CSACTION_5) || ((csAction == PLAYER_CSACTION_END) && (player->csAction == PLAYER_CSACTION_NONE))) { return false; } player->csAction = csAction; player->csActor = csActor; player->cv.haltActorsDuringCsAction = false; return true; } /** * Sets a Player Cutscene Action specified by `csAction`. * * `haltActorsDuringCsAction` being set to true in this function means that eventually `PLAYER_STATE1_20000000` will be * set. This makes it so actors belonging to categories `ACTORCAT_ENEMY` and `ACTORCAT_MISC` will not update while * Player is performing the cutscene action. * * Note: due to how player implements initializing the cutscene action state, `haltActorsDuringCsAction` * will only be considered the first time player starts a `csAction`. * Player must leave the cutscene action state and enter it again before halting actors can be toggled. */ s32 Player_SetCsActionWithHaltedActors(PlayState* play, Actor* csActor, u8 csAction) { Player* player = GET_PLAYER(play); if (Player_SetCsAction(play, csActor, csAction)) { player->cv.haltActorsDuringCsAction = true; return true; } return false; } // Unused void func_800B72E0(DynaPolyActor* dyna) { dyna->unk14C = 0.0f; dyna->pushForce = 0.0f; } void func_800B72F8(DynaPolyActor* dyna, f32 extraPushForce, s16 yRotation) { dyna->yRotation = yRotation; dyna->pushForce += extraPushForce; } /** * Check if the player is facing the specified actor. * The maximum angle difference that qualifies as "facing" is specified by `maxAngleDiff`. */ s32 Player_IsFacingActor(Actor* actor, s16 maxAngleDiff, PlayState* play) { Player* player = GET_PLAYER(play); s16 yawDiff = BINANG_ADD(actor->yawTowardsPlayer, 0x8000) - player->actor.shape.rot.y; if (ABS_ALT(yawDiff) < maxAngleDiff) { return true; } return false; } /** * Check if `actorB` is facing `actorA`. * The maximum angle difference that qualifies as "facing" is specified by `maxAngle`. * * This function is unused in the original game. */ s32 Actor_ActorBIsFacingActorA(Actor* actorA, Actor* actorB, s16 maxAngleDiff) { s16 angle = BINANG_ROT180(Actor_WorldYawTowardActor(actorA, actorB)); s16 dist = angle - actorB->shape.rot.y; if (ABS_ALT(dist) < maxAngleDiff) { return true; } return false; } /** * Check if the specified actor is facing the player. * The maximum angle difference that qualifies as "facing" is specified by `maxAngleDiff`. */ s32 Actor_IsFacingPlayer(Actor* actor, s16 angle) { s16 dist = actor->yawTowardsPlayer - actor->shape.rot.y; if (ABS_ALT(dist) < angle) { return true; } return false; } /** * Check if `actorA` is facing `actorB`. * The maximum angle difference that qualifies as "facing" is specified by `maxAngleDiff`. */ s32 Actor_ActorAIsFacingActorB(Actor* actorA, Actor* actorB, s16 maxAngleDiff) { s16 dist = Actor_WorldYawTowardActor(actorA, actorB) - actorA->shape.rot.y; if (ABS_ALT(dist) < maxAngleDiff) { return true; } return false; } /** * Check if the specified actor is facing the player and is nearby. * The maximum angle difference that qualifies as "facing" is specified by `maxAngleDiff`. * The maximum distance that qualifies as "nearby" is specified by `range`. */ s32 Actor_IsFacingAndNearPlayer(Actor* actor, f32 range, s16 maxAngleDiff) { s16 yaw = actor->yawTowardsPlayer - actor->shape.rot.y; if (ABS_ALT(yaw) < maxAngleDiff) { s16 pad; if (sqrtf(SQ(actor->xzDistToPlayer) + SQ(actor->playerHeightRel)) < range) { return true; } } return false; } /** * Check if `actorA` is facing `actorB` and is nearby. * The maximum angle difference that qualifies as "facing" is specified by `maxAngleDiff`. * The maximum distance that qualifies as "nearby" is specified by `range`. */ s32 Actor_ActorAIsFacingAndNearActorB(Actor* actorA, Actor* actorB, f32 range, s16 maxAngleDiff) { if (Actor_WorldDistXYZToActor(actorA, actorB) < range) { s16 dist = Actor_WorldYawTowardActor(actorA, actorB) - actorA->shape.rot.y; if (ABS_ALT(dist) < maxAngleDiff) { return true; } } return false; } /* Start of BgCheck related section */ void Actor_GetSlopeDirection(CollisionPoly* floorPoly, Vec3f* slopeNormal, s16* downwardSlopeYaw) { slopeNormal->x = COLPOLY_GET_NORMAL(floorPoly->normal.x); slopeNormal->y = COLPOLY_GET_NORMAL(floorPoly->normal.y); slopeNormal->z = COLPOLY_GET_NORMAL(floorPoly->normal.z); *downwardSlopeYaw = Math_Atan2S_XY(slopeNormal->z, slopeNormal->x); } s32 func_800B761C(Actor* actor, f32 arg1, s32 updBgCheckInfoFlags) { if (actor->bgCheckFlags & BGCHECKFLAG_GROUND) { actor->bgCheckFlags &= ~BGCHECKFLAG_GROUND; actor->bgCheckFlags |= BGCHECKFLAG_GROUND_LEAVE; if ((actor->velocity.y < 0.0f) && (updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_10)) { actor->velocity.y = 0.0f; } return false; } return true; } s32 func_800B7678(PlayState* play, Actor* actor, Vec3f* pos, s32 updBgCheckInfoFlags) { f32 distToFloor; s32 bgId; pos->y += (updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_800) ? 10.0f : 50.0f; actor->floorHeight = BgCheck_EntityRaycastFloor5_2(play, &play->colCtx, &actor->floorPoly, &bgId, actor, pos); actor->bgCheckFlags &= ~(BGCHECKFLAG_GROUND_TOUCH | BGCHECKFLAG_GROUND_LEAVE | BGCHECKFLAG_GROUND_STRICT); if (actor->floorHeight <= BGCHECK_Y_MIN) { return func_800B761C(actor, BGCHECK_Y_MIN, updBgCheckInfoFlags); } distToFloor = actor->floorHeight - actor->world.pos.y; actor->floorBgId = bgId; if ((distToFloor >= 0.0f) || (((actor->bgCheckFlags & BGCHECKFLAG_GROUND)) && !(actor->bgCheckFlags & BGCHECKFLAG_PLAYER_800) && (distToFloor >= -11.0f) && (actor->velocity.y < 0.0f))) { actor->bgCheckFlags |= BGCHECKFLAG_GROUND_STRICT; if (actor->bgCheckFlags & BGCHECKFLAG_CEILING) { if (bgId != D_801ED8B4) { if (distToFloor > 15.0f) { actor->bgCheckFlags |= BGCHECKFLAG_CRUSHED; } } else { actor->world.pos.x = actor->prevPos.x; actor->world.pos.z = actor->prevPos.z; } } actor->world.pos.y = actor->floorHeight; if (actor->velocity.y <= 0.0f) { if (!(actor->bgCheckFlags & BGCHECKFLAG_GROUND)) { actor->bgCheckFlags |= BGCHECKFLAG_GROUND_TOUCH; } else if ((updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_8) && (actor->gravity < 0.0f)) { actor->velocity.y = -4.0f; } else if (!(updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_100)) { actor->velocity.y = 0.0f; } actor->bgCheckFlags |= BGCHECKFLAG_GROUND; DynaPolyActor_AttachCarriedActor(&play->colCtx, actor, actor->floorBgId); } } else { return func_800B761C(actor, distToFloor, updBgCheckInfoFlags); } return true; } void Actor_UpdateBgCheckInfo(PlayState* play, Actor* actor, f32 wallCheckHeight, f32 wallCheckRadius, f32 ceilingCheckHeight, u32 updBgCheckInfoFlags) { f32 sp94 = actor->world.pos.y - actor->prevPos.y; s32 pad; Vec3f pos; if ((actor->floorBgId != BGCHECK_SCENE) && (actor->bgCheckFlags & BGCHECKFLAG_GROUND)) { DynaPolyActor_TransformCarriedActor(&play->colCtx, actor->floorBgId, actor); } if (updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_1) { s32 bgId; actor->bgCheckFlags &= ~BGCHECKFLAG_PLAYER_1000; if ((!(updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_80) && (BgCheck_EntitySphVsWall3(&play->colCtx, &pos, &actor->world.pos, &actor->prevPos, wallCheckRadius, &actor->wallPoly, &bgId, actor, wallCheckHeight))) || ((updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_80) && (BgCheck_EntitySphVsWall4(&play->colCtx, &pos, &actor->world.pos, &actor->prevPos, wallCheckRadius, &actor->wallPoly, &bgId, actor, wallCheckHeight)))) { CollisionPoly* sp7C = actor->wallPoly; actor->bgCheckFlags |= BGCHECKFLAG_WALL; if ((updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_200) && (actor->bgCheckFlags & BGCHECKFLAG_PLAYER_1000) && ((s32)sp7C->normal.y > 0) && (sqrtf(SQXYZ(actor->colChkInfo.displacement)) < 10.0f)) { actor->bgCheckFlags &= ~BGCHECKFLAG_WALL; } else if (actor->bgCheckFlags & BGCHECKFLAG_WALL) { Math_Vec3f_Copy(&actor->world.pos, &pos); } actor->wallYaw = Math_Atan2S_XY(sp7C->normal.z, sp7C->normal.x); actor->wallBgId = bgId; } else { actor->bgCheckFlags &= ~BGCHECKFLAG_WALL; } } pos.x = actor->world.pos.x; pos.z = actor->world.pos.z; if (updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_2) { f32 y; pos.y = actor->prevPos.y + 4.0f; if (BgCheck_EntityCheckCeiling(&play->colCtx, &y, &pos, (ceilingCheckHeight + sp94) - 4.0f, &D_801ED8B0, &D_801ED8B4, actor)) { actor->bgCheckFlags |= BGCHECKFLAG_CEILING; actor->world.pos.y = (y + sp94) - 4.0f; } else { actor->bgCheckFlags &= ~BGCHECKFLAG_CEILING; } } if (updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_4) { WaterBox* waterbox; f32 y; pos.y = actor->prevPos.y; func_800B7678(play, actor, &pos, updBgCheckInfoFlags); y = actor->world.pos.y; if (WaterBox_GetSurface1(play, &play->colCtx, actor->world.pos.x, actor->world.pos.z, &y, &waterbox)) { actor->depthInWater = y - actor->world.pos.y; if (actor->depthInWater <= 0.0f) { actor->bgCheckFlags &= ~(BGCHECKFLAG_WATER | BGCHECKFLAG_WATER_TOUCH); } else if (!(actor->bgCheckFlags & BGCHECKFLAG_WATER)) { actor->bgCheckFlags |= (BGCHECKFLAG_WATER | BGCHECKFLAG_WATER_TOUCH); if (!(updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_40)) { Vec3f sp64; sp64.x = actor->world.pos.x; sp64.y = y; sp64.z = actor->world.pos.z; EffectSsGRipple_Spawn(play, &sp64, 100, 500, 0); EffectSsGRipple_Spawn(play, &sp64, 100, 500, 4); EffectSsGRipple_Spawn(play, &sp64, 100, 500, 8); } } else { actor->bgCheckFlags &= ~BGCHECKFLAG_WATER_TOUCH; } } else { actor->bgCheckFlags &= ~(BGCHECKFLAG_WATER | BGCHECKFLAG_WATER_TOUCH); actor->depthInWater = BGCHECK_Y_MIN; } } if (updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_400) { WaterBox* waterbox; f32 y = actor->world.pos.y; if (WaterBox_GetSurface1(play, &play->colCtx, actor->world.pos.x, actor->world.pos.z, &y, &waterbox)) { actor->depthInWater = y - actor->world.pos.y; if (actor->depthInWater < 0.0f) { actor->bgCheckFlags &= ~(BGCHECKFLAG_WATER | BGCHECKFLAG_WATER_TOUCH); } else if (!(actor->bgCheckFlags & BGCHECKFLAG_WATER)) { actor->bgCheckFlags |= (BGCHECKFLAG_WATER | BGCHECKFLAG_WATER_TOUCH); if (!(updBgCheckInfoFlags & UPDBGCHECKINFO_FLAG_40)) { Vec3f sp50; sp50.x = actor->world.pos.x; sp50.y = y; sp50.z = actor->world.pos.z; EffectSsGRipple_Spawn(play, &sp50, 100, 500, 0); EffectSsGRipple_Spawn(play, &sp50, 100, 500, 4); EffectSsGRipple_Spawn(play, &sp50, 100, 500, 8); } } else { actor->bgCheckFlags &= ~BGCHECKFLAG_WATER_TOUCH; } } else { actor->bgCheckFlags &= ~(BGCHECKFLAG_WATER | BGCHECKFLAG_WATER_TOUCH); actor->depthInWater = BGCHECK_Y_MIN; } } } Gfx* Hilite_Draw(Vec3f* object, Vec3f* eye, Vec3f* lightDir, GraphicsContext* gfxCtx, Gfx* gfx, Hilite** hiliteP) { LookAt* lookAt = GRAPH_ALLOC(gfxCtx, sizeof(LookAt)); f32 correctedEyeX = (eye->x == object->x) && (eye->z == object->z) ? eye->x + 0.001f : eye->x; *hiliteP = GRAPH_ALLOC(gfxCtx, sizeof(Hilite)); guLookAtHilite(&sActorHiliteMtx, lookAt, *hiliteP, correctedEyeX, eye->y, eye->z, object->x, object->y, object->z, 0.0f, 1.0f, 0.0f, lightDir->x, lightDir->y, lightDir->z, lightDir->x, lightDir->y, lightDir->z, 0x10, 0x10); gSPLookAt(gfx++, lookAt); gDPSetHilite1Tile(gfx++, 1, *hiliteP, 0x10, 0x10); return gfx; } Hilite* Hilite_DrawOpa(Vec3f* object, Vec3f* eye, Vec3f* lightDir, GraphicsContext* gfxCtx) { Hilite* hilite; OPEN_DISPS(gfxCtx); POLY_OPA_DISP = Hilite_Draw(object, eye, lightDir, gfxCtx, POLY_OPA_DISP, &hilite); CLOSE_DISPS(gfxCtx); return hilite; } Hilite* Hilite_DrawXlu(Vec3f* object, Vec3f* eye, Vec3f* lightDir, GraphicsContext* gfxCtx) { Hilite* hilite; OPEN_DISPS(gfxCtx); POLY_XLU_DISP = Hilite_Draw(object, eye, lightDir, gfxCtx, POLY_XLU_DISP, &hilite); CLOSE_DISPS(gfxCtx); return hilite; } void func_800B8050(Actor* actor, PlayState* play, s32 flag) { Hilite* hilite = func_800BCBF4(&actor->world.pos, play); if (flag != 0) { Gfx* gfxHead = GRAPH_ALLOC(play->state.gfxCtx, 2 * sizeof(Gfx)); Gfx* gfx = gfxHead; OPEN_DISPS(play->state.gfxCtx); gDPSetHilite1Tile(gfx++, 1, hilite, 0x10, 0x10); gSPEndDisplayList(gfx++); gSPSegment(POLY_OPA_DISP++, 0x07, gfxHead); CLOSE_DISPS(play->state.gfxCtx); } } void func_800B8118(Actor* actor, PlayState* play, s32 flag) { Hilite* hilite = func_800BCC68(&actor->world.pos, play); if (flag != 0) { Gfx* gfxHead = GRAPH_ALLOC(play->state.gfxCtx, 2 * sizeof(Gfx)); Gfx* gfx = gfxHead; OPEN_DISPS(play->state.gfxCtx); gDPSetHilite1Tile(gfx++, 1, hilite, 0x10, 0x10); gSPEndDisplayList(gfx++); gSPSegment(POLY_XLU_DISP++, 0x07, gfxHead); CLOSE_DISPS(play->state.gfxCtx); } } PosRot Actor_GetFocus(Actor* actor) { return actor->focus; } PosRot Actor_GetWorld(Actor* actor) { return actor->world; } PosRot Actor_GetWorldPosShapeRot(Actor* actor) { PosRot sp1C; Math_Vec3f_Copy(&sp1C.pos, &actor->world.pos); if (actor->id == ACTOR_PLAYER) { Player* player = (Player*)actor; sp1C.pos.y += player->unk_AC0 * actor->scale.y; } sp1C.rot = actor->shape.rot; return sp1C; } /** * Returns the squared xyz distance from the actor to Player. * This distance will be weighted if Player is already locked onto another actor. */ f32 Attention_WeightedDistToPlayerSq(Actor* actor, Player* player, s16 playerShapeYaw) { f32 adjDistSq; s16 yawDiffAbs = ABS_ALT(BINANG_SUB(BINANG_SUB(actor->yawTowardsPlayer, 0x8000), playerShapeYaw)); if (player->focusActor != NULL) { if ((yawDiffAbs > 0x4000) || (actor->flags & ACTOR_FLAG_LOCK_ON_DISABLED)) { return FLT_MAX; } // The distance returned is scaled down as the player faces more toward the actor. // At 90 degrees, 100% of the original distance will be returned. // This scales down linearly to 60% when facing 0 degrees away. adjDistSq = actor->xyzDistToPlayerSq - ((actor->xyzDistToPlayerSq * 0.8f) * ((0x4000 - yawDiffAbs) * (1.0f / 0x8000))); return adjDistSq; } // Player has to be facing less than ~60 degrees away from the actor if (yawDiffAbs > (0x10000 / 6)) { return FLT_MAX; } // Unweighted distSq return actor->xyzDistToPlayerSq; } #define ATTENTION_RANGES(range, lockOnLeashRange) \ { SQ(range), (f32)(range) / (lockOnLeashRange) } AttentionRangeParams gAttentionRanges[ATTENTION_RANGE_MAX] = { ATTENTION_RANGES(70, 140), // ATTENTION_RANGE_0 ATTENTION_RANGES(170, 255), // ATTENTION_RANGE_1 ATTENTION_RANGES(280, 5600), // ATTENTION_RANGE_2 ATTENTION_RANGES(350, 525), // ATTENTION_RANGE_3 ATTENTION_RANGES(700, 1050), // ATTENTION_RANGE_4 ATTENTION_RANGES(1000, 1500), // ATTENTION_RANGE_5 ATTENTION_RANGES(100, 105.36842), // ATTENTION_RANGE_6 ATTENTION_RANGES(140, 163.33333), // ATTENTION_RANGE_7 ATTENTION_RANGES(240, 576), // ATTENTION_RANGE_8 ATTENTION_RANGES(280, 280000), // ATTENTION_RANGE_9 ATTENTION_RANGES(2500, 3750), // ATTENTION_RANGE_10 }; /** * Checks if an actor at `distSq` is inside the range specified by its `attentionRangeType`. * * Note that this gets used for both the attention range check and for the lock-on leash range check. * Despite how the data is presented in `gAttentionRanges`, the leash range is stored as a scale factor value. * When checking the leash range, this scale factor is applied to the input distance and checked against * the base `attentionRangeSq` value, which was used to initiate the lock-on in the first place. */ s32 Attention_ActorIsInRange(Actor* actor, f32 distSq) { return distSq < gAttentionRanges[actor->attentionRangeType].attentionRangeSq; } /** * Returns true if an actor lock-on should be released. * This function does not actually release the lock-on, as that is Player's responsibility. * * If an actor's update function is NULL or `ACTOR_FLAG_ATTENTION_ENABLED` is unset, the lock-on should be released. * * There is also a check for Player exceeding the lock-on leash distance. * Note that this check will be ignored if `ignoreLeash` is true. * */ s32 Attention_ShouldReleaseLockOn(Actor* actor, Player* player, s32 ignoreLeash) { if ((actor->update == NULL) || !(actor->flags & ACTOR_FLAG_ATTENTION_ENABLED) || (actor->flags & ACTOR_FLAG_LOCK_ON_DISABLED)) { return true; } if (!ignoreLeash) { s16 yawDiffAbs = ABS_ALT(BINANG_SUB(BINANG_SUB(actor->yawTowardsPlayer, 0x8000), player->actor.shape.rot.y)); f32 distSq; if ((player->focusActor == NULL) && (yawDiffAbs > (0x10000 / 6))) { // This function is only called (and is only relevant) when `player->focusActor != NULL`. // This is unreachable. distSq = FLT_MAX; } else { distSq = actor->xyzDistToPlayerSq; } return !Attention_ActorIsInRange(actor, gAttentionRanges[actor->attentionRangeType].lockOnLeashScale * distSq); } return false; } s16 D_801AED48[] = { HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY4_NIGHT, HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY1_NIGHT | HALFDAYBIT_DAY4_NIGHT, HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY2_NIGHT | HALFDAYBIT_DAY4_NIGHT, HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY1_NIGHT | HALFDAYBIT_DAY2_NIGHT | HALFDAYBIT_DAY4_NIGHT, HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY3_NIGHT | HALFDAYBIT_DAY4_NIGHT, HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY1_NIGHT | HALFDAYBIT_DAY3_NIGHT | HALFDAYBIT_DAY4_NIGHT, HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY2_NIGHT | HALFDAYBIT_DAY3_NIGHT | HALFDAYBIT_DAY4_NIGHT, HALFDAYBIT_DAY0_NIGHT | HALFDAYBIT_DAY1_NIGHT | HALFDAYBIT_DAY2_NIGHT | HALFDAYBIT_DAY3_NIGHT | HALFDAYBIT_DAY4_NIGHT, }; /** * When a given talk offer is accepted, Player will set `ACTOR_FLAG_TALK` for that actor. * This function serves to acknowledge that the offer was accepted by Player, and notifies the actor * that it should proceed with its own internal processes for handling dialogue. * * @return true if the talk offer was accepted, false otherwise */ s32 Actor_TalkOfferAccepted(Actor* actor, GameState* gameState) { if (actor->flags & ACTOR_FLAG_TALK) { actor->flags &= ~ACTOR_FLAG_TALK; return true; } return false; } /** * This function covers offering the ability to `Talk` with the player. * Passing an exchangeItemAction (see `PlayerItemAction`) allows the player to also use the item to initiate the * conversation. * * This function carries a talk exchange request to the player actor if context allows it (e.g. the player is in range * and not busy with certain things). The player actor performs the requested action itself. * * The following description of what the `exchangeItemAction` values can do is provided here for completeness, but these * behaviors are entirely out of the scope of this function. All behavior is defined by the player actor. * * - Positive values (`PLAYER_IA_NONE < exchangeItemAction < PLAYER_IA_MAX`): * Offers the ability to initiate the conversation with an item from the player. * Not all positive values are implemented properly for this to work. * Working ones are PLAYER_IA_PICTOGRAPH_BOX and PLAYER_IA_BOTTLE_MIN <= exchangeItemAction < PLAYER_IA_MASK_MIN * Note: While PLAYER_IA_MAGIC_BEANS works, it is special cased to just plant the bean with no talking. * - `PLAYER_IA_NONE`: * Allows the player to speak to or check the actor (by pressing A). * - `PLAYER_IA_MINUS1`: * Used by actors/player to continue the current conversation after a textbox is closed. * * @return true If the player actor is capable of accepting the offer. * * Note: There is only one instance of using this for actually using an item to start the conversation with the player. * Every other instance is to either offer to speak, or continue the current conversation. */ s32 Actor_OfferTalkExchange(Actor* actor, PlayState* play, f32 xzRange, f32 yRange, PlayerItemAction exchangeItemAction) { Player* player = GET_PLAYER(play); if ((player->actor.flags & ACTOR_FLAG_TALK) || ((exchangeItemAction > PLAYER_IA_NONE) && Player_InCsMode(play)) || (!actor->isLockedOn && ((fabsf(actor->playerHeightRel) > fabsf(yRange)) || (actor->xzDistToPlayer > player->talkActorDistance) || (xzRange < actor->xzDistToPlayer)))) { return false; } player->talkActor = actor; player->talkActorDistance = actor->xzDistToPlayer; player->exchangeItemAction = exchangeItemAction; CutsceneManager_Queue(CS_ID_GLOBAL_TALK); return true; } /** * Offers a talk exchange request within an equilateral cylinder with the radius specified. */ s32 Actor_OfferTalkExchangeEquiCylinder(Actor* actor, PlayState* play, f32 radius, PlayerItemAction exchangeItemAction) { return Actor_OfferTalkExchange(actor, play, radius, radius, exchangeItemAction); } /** * Offers a talk request within an equilateral cylinder with the radius specified. */ s32 Actor_OfferTalk(Actor* actor, PlayState* play, f32 radius) { return Actor_OfferTalkExchangeEquiCylinder(actor, play, radius, PLAYER_IA_NONE); } /** * Offers a talk request within an equilateral cylinder whose radius is determined by the actor's collision check * cylinder's radius. */ s32 Actor_OfferTalkNearColChkInfoCylinder(Actor* actor, PlayState* play) { f32 cylRadius = actor->colChkInfo.cylRadius + 50.0f; return Actor_OfferTalk(actor, play, cylRadius); } s32 Actor_TextboxIsClosing(Actor* actor, PlayState* play) { if (Message_GetState(&play->msgCtx) == TEXT_STATE_CLOSING) { actor->flags &= ~ACTOR_FLAG_TALK; return true; } return false; } /** * Changes the actor the Player is focussing on * Fails if Player is not already focussing on an actor or in a talking state */ s32 Actor_ChangeFocus(Actor* actor1, PlayState* play, Actor* actor2) { Actor* talkActor; Player* player = GET_PLAYER(play); talkActor = player->talkActor; if ((player->actor.flags & ACTOR_FLAG_TALK) && (talkActor != NULL)) { player->talkActor = actor2; player->focusActor = actor2; return true; } return false; } PlayerItemAction Player_GetExchangeItemAction(PlayState* play) { Player* player = GET_PLAYER(play); return player->exchangeItemAction; } /** * When a given ocarina interaction offer is accepted, Player will set `ACTOR_FLAG_OCARINA_INTERACTION` for that actor. * An exception is made for EN_ZOT, see `Player_ActionHandler_13`. * This function serves to acknowledge that the offer was accepted by Player, and notifies the actor * that it should proceed with its own internal processes for handling further interactions. * * @return true if the ocarina interaction offer was accepted, false otherwise */ s32 Actor_OcarinaInteractionAccepted(Actor* actor, GameState* gameState) { if (actor->flags & ACTOR_FLAG_OCARINA_INTERACTION) { actor->flags &= ~ACTOR_FLAG_OCARINA_INTERACTION; return true; } return false; } s32 Actor_OfferOcarinaInteraction(Actor* actor, PlayState* play, f32 xzRange, f32 yRange) { Player* player = GET_PLAYER(play); if ((player->actor.flags & ACTOR_FLAG_OCARINA_INTERACTION) || Player_InCsMode(play) || (yRange < fabsf(actor->playerHeightRel)) || ((player->ocarinaInteractionDistance < actor->xzDistToPlayer)) || (xzRange < actor->xzDistToPlayer)) { return false; } player->ocarinaInteractionActor = actor; player->ocarinaInteractionDistance = actor->xzDistToPlayer; return true; } s32 Actor_OfferOcarinaInteractionNearby(Actor* actor, PlayState* play, f32 xzRange) { return Actor_OfferOcarinaInteraction(actor, play, xzRange, 20.0f); } s32 Actor_OfferOcarinaInteractionColChkInfoCylinder(Actor* actor, PlayState* play) { f32 cylRadius = actor->colChkInfo.cylRadius + 50.0f; return Actor_OfferOcarinaInteractionNearby(actor, play, cylRadius); } s32 Actor_NoOcarinaInteraction(Actor* actor, PlayState* play) { if (!(GET_PLAYER(play)->actor.flags & ACTOR_FLAG_OCARINA_INTERACTION)) { return true; } return false; } void Actor_GetScreenPos(PlayState* play, Actor* actor, s16* posX, s16* posY) { Vec3f projectedPos; f32 invW; Actor_GetProjectedPos(play, &actor->focus.pos, &projectedPos, &invW); *posX = PROJECTED_TO_SCREEN_X(projectedPos, invW); *posY = PROJECTED_TO_SCREEN_Y(projectedPos, invW); } bool Actor_OnScreen(PlayState* play, Actor* actor) { Vec3f projectedPos; f32 invW; s32 pad[2]; Actor_GetProjectedPos(play, &actor->focus.pos, &projectedPos, &invW); return (projectedPos.x * invW >= -1.0f) && (projectedPos.x * invW <= 1.0f) && (projectedPos.y * invW >= -1.0f) && (projectedPos.y * invW <= 1.0f); } s32 Actor_HasParent(Actor* actor, PlayState* play) { if (actor->parent != NULL) { return true; } else { return false; } } /** * This function covers various interactions with the player actor, using Get Item IDs (see `GetItemID` enum). * It is typically used to give items to the player, but also has other purposes. * * This function carries a get item request to the player actor if context allows it (e.g. the player is in range and * not busy with certain things). The player actor performs the requested action itself. * * The following description of what the `getItemId` values can do is provided here for completeness, but these * behaviors are entirely out of the scope of this function. All behavior is defined by the player actor. * * - Positive values (`GI_NONE < getItemId < GI_MAX`): * Give an item to the player. The player may not get it immediately (for example if diving), but is expected to * in the near future. * - Negative values (`-GI_MAX < getItemId < GI_NONE`): * Used by treasure chests to indicate the chest can be opened (by pressing A). * The item gotten corresponds to the positive Get Item ID `abs(getItemId)`. * - `GI_NONE`: * Allows the player to pick up the actor (by pressing A), to carry it around. * - `GI_MAX`: * Allows the player to catch specific actors in a bottle. * * @return true If the player actor is capable of accepting the offer. */ s32 Actor_OfferGetItem(Actor* actor, PlayState* play, GetItemId getItemId, f32 xzRange, f32 yRange) { Player* player = GET_PLAYER(play); if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_CHARGING_SPIN_ATTACK | PLAYER_STATE1_2000 | PLAYER_STATE1_4000 | PLAYER_STATE1_40000 | PLAYER_STATE1_80000 | PLAYER_STATE1_100000 | PLAYER_STATE1_200000)) && (Player_GetExplosiveHeld(player) <= PLAYER_EXPLOSIVE_NONE)) { if ((actor->xzDistToPlayer <= xzRange) && (fabsf(actor->playerHeightRel) <= fabsf(yRange))) { if (((getItemId == GI_MASK_CIRCUS_LEADER) || (getItemId == GI_PENDANT_OF_MEMORIES) || (getItemId == GI_DEED_LAND) || (((player->heldActor != NULL) || (actor == player->talkActor)) && ((getItemId > GI_NONE) && (getItemId < GI_MAX)))) || !(player->stateFlags1 & (PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_20000000))) { s16 yawDiff = actor->yawTowardsPlayer - player->actor.shape.rot.y; s32 absYawDiff = ABS_ALT(yawDiff); if ((getItemId != GI_NONE) || (player->getItemDirection < absYawDiff)) { player->getItemId = getItemId; player->interactRangeActor = actor; player->getItemDirection = absYawDiff; if ((getItemId > GI_NONE) && (getItemId < GI_MAX)) { CutsceneManager_Queue(play->playerCsIds[PLAYER_CS_ID_ITEM_GET]); } return true; } } } } return false; } s32 Actor_OfferGetItemNearby(Actor* actor, PlayState* play, GetItemId getItemId) { return Actor_OfferGetItem(actor, play, getItemId, 50.0f, 10.0f); } s32 Actor_OfferCarry(Actor* actor, PlayState* play) { return Actor_OfferGetItemNearby(actor, play, GI_NONE); } s32 Actor_OfferGetItemFar(Actor* actor, PlayState* play, GetItemId getItemId) { return Actor_OfferGetItem(actor, play, getItemId, 9999.9f, 9999.9f); } s32 Actor_HasNoParent(Actor* actor, PlayState* play) { if (actor->parent == NULL) { return true; } return false; } void func_800B8C20(Actor* actorA, Actor* actorB, PlayState* play) { Actor* parent = actorA->parent; if (parent->id == ACTOR_PLAYER) { Player* player = (Player*)parent; player->heldActor = actorB; player->interactRangeActor = actorB; } parent->child = actorB; actorB->parent = parent; actorA->parent = NULL; } /** * Sets closest secret distance to the distance to the actor. Calling this function on `actor` is the way to make it a * 'secret' for that update cycle, i.e. something that the controller will rumble for. */ void Actor_SetClosestSecretDistance(Actor* actor, PlayState* play) { Player* player = GET_PLAYER(play); if (actor->xyzDistToPlayerSq < player->closestSecretDistSq) { player->closestSecretDistSq = actor->xyzDistToPlayerSq; } } s32 Actor_HasRider(PlayState* play, Actor* horse) { if (horse->child != NULL) { return true; } return false; } s32 Actor_SetRideActor(PlayState* play, Actor* horse, s32 mountSide) { Player* player = GET_PLAYER(play); if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_CHARGING_SPIN_ATTACK | PLAYER_STATE1_2000 | PLAYER_STATE1_4000 | PLAYER_STATE1_40000 | PLAYER_STATE1_80000 | PLAYER_STATE1_100000 | PLAYER_STATE1_200000))) { player->rideActor = horse; player->mountSide = mountSide; CutsceneManager_Queue(CS_ID_GLOBAL_TALK); return true; } return false; } s32 Actor_HasNoRider(PlayState* play, Actor* horse) { if (horse->child == NULL) { return true; } return false; } void func_800B8D10(PlayState* play, Actor* actor, f32 arg2, s16 arg3, f32 arg4, s32 arg5, u32 arg6) { Player* player = GET_PLAYER(play); player->unk_B74 = arg6; player->unk_B75 = arg5; player->unk_B78 = arg2; player->unk_B76 = arg3; player->unk_B7C = arg4; } void func_800B8D50(PlayState* play, Actor* actor, f32 arg2, s16 yaw, f32 arg4, u32 arg5) { func_800B8D10(play, actor, arg2, yaw, arg4, 3, arg5); } void func_800B8D98(PlayState* play, Actor* actor, f32 arg2, s16 arg3, f32 arg4) { func_800B8D50(play, actor, arg2, arg3, arg4, 0); } void func_800B8DD4(PlayState* play, Actor* actor, f32 arg2, s16 arg3, f32 arg4, u32 arg5) { func_800B8D10(play, actor, arg2, arg3, arg4, 2, arg5); } void func_800B8E1C(PlayState* play, Actor* actor, f32 arg2, s16 arg3, f32 arg4) { func_800B8DD4(play, actor, arg2, arg3, arg4, 0); } /** * Play a sound effect at the player's position */ void Player_PlaySfx(Player* player, u16 sfxId) { if (player->currentMask == PLAYER_MASK_GIANT) { Audio_PlaySfx_AtPosWithPresetLowFreqAndHighReverb(&player->actor.projectedPos, sfxId); } else { AudioSfx_PlaySfx(sfxId, &player->actor.projectedPos, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); } } /** * Play a sound effect at the actor's position */ void Actor_PlaySfx(Actor* actor, u16 sfxId) { Audio_PlaySfx_AtPos(&actor->projectedPos, sfxId); } void Actor_PlaySfx_SurfaceBomb(PlayState* play, Actor* actor) { SurfaceSfxOffset surfaceSfxOffset; if (actor->bgCheckFlags & BGCHECKFLAG_WATER) { if (actor->depthInWater < 20.0f) { surfaceSfxOffset = SURFACE_SFX_OFFSET_WATER_SHALLOW; } else { surfaceSfxOffset = SURFACE_SFX_OFFSET_WATER_DEEP; } } else { surfaceSfxOffset = SurfaceType_GetSfxOffset(&play->colCtx, actor->floorPoly, actor->floorBgId); } Audio_PlaySfx_AtPos(&actor->projectedPos, NA_SE_EV_BOMB_BOUND); Audio_PlaySfx_AtPos(&actor->projectedPos, NA_SE_PL_WALK_GROUND + surfaceSfxOffset); } /** * Play a sfx at the actor's position using the shared audioFlag system */ void Actor_PlaySfx_Flagged2(Actor* actor, u16 sfxId) { actor->sfxId = sfxId; actor->audioFlags &= ~ACTOR_AUDIO_FLAG_SFX_ALL; actor->audioFlags |= ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_2; } /** * Play a sfx at the center of the screen using the shared audioFlag system */ void Actor_PlaySfx_FlaggedCentered1(Actor* actor, u16 sfxId) { actor->sfxId = sfxId; actor->audioFlags &= ~ACTOR_AUDIO_FLAG_SFX_ALL; actor->audioFlags |= ACTOR_AUDIO_FLAG_SFX_CENTERED_1; } /** * Play a sfx at the center of the screen using the shared audioFlag system */ void Actor_PlaySfx_FlaggedCentered2(Actor* actor, u16 sfxId) { actor->sfxId = sfxId; actor->audioFlags &= ~ACTOR_AUDIO_FLAG_SFX_ALL; actor->audioFlags |= ACTOR_AUDIO_FLAG_SFX_CENTERED_2; } /** * Play a sfx at the actor's position using the shared audioFlag system */ void Actor_PlaySfx_Flagged(Actor* actor, u16 sfxId) { actor->sfxId = sfxId; actor->audioFlags &= ~ACTOR_AUDIO_FLAG_SFX_ALL; actor->audioFlags |= ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_1; } void Actor_PlaySfx_FlaggedTimer(Actor* actor, s32 timer) { actor->audioFlags &= ~ACTOR_AUDIO_FLAG_SFX_ALL; actor->audioFlags |= ACTOR_AUDIO_FLAG_SFX_TIMER; // The sfxId here are not actually sound effects, but instead this is data that gets sent into // the io ports of the music macro language (Audio_PlaySfx_AtPosWithChannelIO / Audio_PlaySfxAtPosWithSoundScriptIO // is the function that it's used for) if (timer < 40) { actor->sfxId = 3; } else if (timer < 100) { actor->sfxId = 2; } else { actor->sfxId = 1; } } void Actor_PlaySeq_FlaggedKamaroDance(Actor* actor) { actor->audioFlags |= ACTOR_AUDIO_FLAG_SEQ_KAMARO_DANCE; } void Actor_PlaySeq_FlaggedMusicBoxHouse(Actor* actor) { actor->audioFlags |= ACTOR_AUDIO_FLAG_SEQ_MUSIC_BOX_HOUSE; } s32 func_800B90AC(PlayState* play, Actor* actor, CollisionPoly* polygon, s32 bgId, Vec3f* arg4) { if (SurfaceType_GetFloorType(&play->colCtx, polygon, bgId) == FLOOR_TYPE_8) { return true; } return false; } void Actor_DeactivateLens(PlayState* play) { if (play->actorCtx.lensActive) { play->actorCtx.lensActive = false; Magic_Reset(play); } } void Actor_InitHalfDaysBit(ActorContext* actorCtx) { s32 halfDayCount = CURRENT_DAY * 2; if ((CURRENT_TIME < CLOCK_TIME(6, 0)) || (CURRENT_TIME > CLOCK_TIME(18, 0))) { halfDayCount++; } actorCtx->halfDaysBit = HALFDAYBIT_DAY0_DAWN >> halfDayCount; } void Actor_InitContext(PlayState* play, ActorContext* actorCtx, ActorEntry* actorEntry) { ActorOverlay* overlayEntry; CycleSceneFlags* cycleFlags; s32 i; SET_WEEKEVENTREG(WEEKEVENTREG_92_80); cycleFlags = &gSaveContext.cycleSceneFlags[Play_GetOriginalSceneId(play->sceneId)]; bzero(actorCtx, sizeof(ActorContext)); ActorOverlayTable_Init(); Matrix_MtxFCopy(&play->billboardMtxF, &gIdentityMtxF); Matrix_MtxFCopy(&play->viewProjectionMtxF, &gIdentityMtxF); overlayEntry = gActorOverlayTable; for (i = 0; i < ARRAY_COUNT(gActorOverlayTable); i++) { overlayEntry->loadedRamAddr = NULL; overlayEntry->numLoaded = 0; overlayEntry++; } actorCtx->sceneFlags.chest = cycleFlags->chest; actorCtx->sceneFlags.switches[0] = cycleFlags->switch0; actorCtx->sceneFlags.switches[1] = cycleFlags->switch1; if (play->sceneId == SCENE_INISIE_R) { cycleFlags = &gSaveContext.cycleSceneFlags[play->sceneId]; } actorCtx->sceneFlags.collectible[0] = cycleFlags->collectible; actorCtx->sceneFlags.clearedRoom = cycleFlags->clearedRoom; TitleCard_ContextInit(&play->state, &actorCtx->titleCtx); Actor_InitPlayerImpact(play); actorCtx->absoluteSpace = NULL; Actor_SpawnEntry(actorCtx, actorEntry, play); Attention_Init(&actorCtx->attention, actorCtx->actorLists[ACTORCAT_PLAYER].first, play); Actor_InitHalfDaysBit(actorCtx); Fault_AddClient(&sActorFaultClient, (void*)Actor_PrintLists, actorCtx, NULL); Player_SpawnHorse(play, (Player*)actorCtx->actorLists[ACTORCAT_PLAYER].first); } /** * Spawns the actors in the current setup (of the current scene/setup/room triple) * Only spawns actors based on the time flags embedded in their rotation parameters */ void Actor_SpawnSetupActors(PlayState* play, ActorContext* actorCtx) { if (play->numSetupActors > 0) { ActorEntry* actorEntry = play->setupActorList; s32 prevHalfDaysBitValue = actorCtx->halfDaysBit; s32 shiftedHalfDaysBit; s32 actorEntryHalfDayBit; s32 i; Actor_InitHalfDaysBit(actorCtx); Actor_KillAllOnHalfDayChange(play, &play->actorCtx); // Shift to previous halfDay bit, but ignoring DAY0_NIGHT. // In other words, if the current halfDay is DAY1_DAY then this logic is ignored and this variable is zero shiftedHalfDaysBit = (actorCtx->halfDaysBit << 1) & (HALFDAYBIT_ALL & ~HALFDAYBIT_DAY0_NIGHT); for (i = 0; i < play->numSetupActors; i++) { actorEntryHalfDayBit = ((actorEntry->rot.x & 7) << 7) | (actorEntry->rot.z & 0x7F); if (actorEntryHalfDayBit == 0) { actorEntryHalfDayBit = HALFDAYBIT_ALL; } if (!(actorEntryHalfDayBit & prevHalfDaysBitValue) && (actorEntryHalfDayBit & actorCtx->halfDaysBit) && (!CHECK_EVENTINF(EVENTINF_17) || !(actorEntryHalfDayBit & shiftedHalfDaysBit) || !(actorEntry->id & 0x800))) { Actor_SpawnEntry(&play->actorCtx, actorEntry, play); } actorEntry++; } // Prevents re-spawning the setup actors play->numSetupActors = -play->numSetupActors; } } typedef struct { /* 0x00 */ PlayState* play; /* 0x04 */ Actor* actor; /* 0x08 */ u32 freezeExceptionFlag; /* 0x0C */ u32 canFreezeCategory; /* 0x10 */ Actor* talkActor; /* 0x14 */ Player* player; /* 0x18 */ u32 updateActorFlagsMask; // Actor will update only if at least 1 actor flag is set in this bitmask } UpdateActor_Params; // size = 0x1C Actor* Actor_UpdateActor(UpdateActor_Params* params) { PlayState* play = params->play; Actor* actor = params->actor; Actor* nextActor; if (actor->world.pos.y < -25000.0f) { actor->world.pos.y = -25000.0f; } actor->sfxId = 0; actor->audioFlags &= ~ACTOR_AUDIO_FLAG_ALL; if (actor->init != NULL) { if (Object_IsLoaded(&play->objectCtx, actor->objectSlot)) { Actor_SetObjectDependency(play, actor); actor->init(actor, play); actor->init = NULL; } nextActor = actor->next; } else if (actor->update == NULL) { if (!actor->isDrawn) { nextActor = Actor_Delete(&play->actorCtx, actor, play); } else { Actor_Destroy(actor, play); nextActor = actor->next; } } else { if (!Object_IsLoaded(&play->objectCtx, actor->objectSlot)) { Actor_Kill(actor); } else if (((params->freezeExceptionFlag != 0) && !(actor->flags & params->freezeExceptionFlag)) || (((!params->freezeExceptionFlag) != 0) && (!(actor->flags & ACTOR_FLAG_FREEZE_EXCEPTION) || ((actor->category == ACTORCAT_EXPLOSIVES) && (params->player->stateFlags1 & PLAYER_STATE1_200))) && params->canFreezeCategory && (actor != params->talkActor) && (actor != params->player->heldActor) && (actor->parent != ¶ms->player->actor))) { CollisionCheck_ResetDamage(&actor->colChkInfo); } else { Math_Vec3f_Copy(&actor->prevPos, &actor->world.pos); actor->xzDistToPlayer = Actor_WorldDistXZToActor(actor, ¶ms->player->actor); actor->playerHeightRel = Actor_HeightDiff(actor, ¶ms->player->actor); actor->xyzDistToPlayerSq = SQ(actor->xzDistToPlayer) + SQ(actor->playerHeightRel); actor->yawTowardsPlayer = Actor_WorldYawTowardActor(actor, ¶ms->player->actor); actor->flags &= ~ACTOR_FLAG_SFX_FOR_PLAYER_BODY_HIT; if ((DECR(actor->freezeTimer) == 0) && (actor->flags & params->updateActorFlagsMask)) { if (actor == params->player->focusActor) { actor->isLockedOn = true; } else { actor->isLockedOn = false; } if ((actor->targetPriority != 0) && (params->player->focusActor == NULL)) { actor->targetPriority = 0; } Actor_SetObjectDependency(play, actor); if (actor->colorFilterTimer != 0) { actor->colorFilterTimer--; } actor->update(actor, play); DynaPoly_UnsetAllInteractFlags(play, &play->colCtx.dyna, actor); } CollisionCheck_ResetDamage(&actor->colChkInfo); } nextActor = actor->next; } return nextActor; } u32 sCategoryFreezeMasks[ACTORCAT_MAX] = { /* ACTORCAT_SWITCH */ PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_10000000, /* ACTORCAT_BG */ PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_10000000, /* ACTORCAT_PLAYER */ PLAYER_STATE1_200, /* ACTORCAT_EXPLOSIVES */ PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_400 | PLAYER_STATE1_10000000, /* ACTORCAT_NPC */ PLAYER_STATE1_2 | PLAYER_STATE1_DEAD | PLAYER_STATE1_200, /* ACTORCAT_ENEMY */ PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_10000000 | PLAYER_STATE1_20000000, /* ACTORCAT_PROP */ PLAYER_STATE1_2 | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_10000000, /* ACTORCAT_ITEMACTION */ PLAYER_STATE1_2, /* ACTORCAT_MISC */ PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_10000000 | PLAYER_STATE1_20000000, /* ACTORCAT_BOSS */ PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_400 | PLAYER_STATE1_10000000, /* ACTORCAT_DOOR */ PLAYER_STATE1_2, /* ACTORCAT_CHEST */ PLAYER_STATE1_2 | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD | PLAYER_STATE1_200 | PLAYER_STATE1_10000000, }; void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { s32 category; Actor* actor; Player* player = GET_PLAYER(play); u32* categoryFreezeMaskP; s32 newCategory; Actor* next; ActorListEntry* entry; UpdateActor_Params params; params.player = player; params.play = play; if (play->soaringCsOrSoTCsPlaying) { params.updateActorFlagsMask = ACTOR_FLAG_UPDATE_DURING_SOARING_AND_SOT_CS; } else { params.updateActorFlagsMask = ACTOR_FLAG_UPDATE_DURING_SOARING_AND_SOT_CS | ACTOR_FLAG_INSIDE_CULLING_VOLUME | ACTOR_FLAG_UPDATE_CULLING_DISABLED; } Actor_SpawnSetupActors(play, actorCtx); if (actorCtx->unk2 != 0) { actorCtx->unk2--; } categoryFreezeMaskP = sCategoryFreezeMasks; if (player->stateFlags2 & PLAYER_STATE2_USING_OCARINA) { params.freezeExceptionFlag = ACTOR_FLAG_UPDATE_DURING_OCARINA; } else { params.freezeExceptionFlag = 0; } if ((player->stateFlags1 & PLAYER_STATE1_TALKING) && ((player->actor.textId & 0xFF00) != 0x1900)) { params.talkActor = player->talkActor; } else { params.talkActor = NULL; } for (category = 0, entry = actorCtx->actorLists; category < ACTORCAT_MAX; entry++, categoryFreezeMaskP++, category++) { params.canFreezeCategory = *categoryFreezeMaskP & player->stateFlags1; params.actor = entry->first; while (params.actor != NULL) { params.actor = Actor_UpdateActor(¶ms); } if (category == ACTORCAT_BG) { DynaPoly_UpdateContext(play, &play->colCtx.dyna); } } // Move actors to a different actorList if it has changed categories. for (category = 0, entry = actorCtx->actorLists; category < ACTORCAT_MAX; entry++, category++) { if (entry->categoryChanged) { actor = entry->first; while (actor != NULL) { if (actor->category == category) { // The actor category matches the list category. No change needed. actor = actor->next; continue; } // The actor category does not match the list category and needs to be moved. next = actor->next; newCategory = actor->category; actor->category = category; Actor_RemoveFromCategory(play, actorCtx, actor); Actor_AddToCategory(actorCtx, actor, newCategory); actor = next; } entry->categoryChanged = false; } } actor = player->focusActor; if ((actor != NULL) && (actor->update == NULL)) { actor = NULL; Player_ReleaseLockOn(player); } if ((actor == NULL) || (player->zTargetActiveTimer < 5)) { actor = NULL; if (actorCtx->attention.reticleSpinCounter != 0) { actorCtx->attention.reticleSpinCounter = 0; Audio_PlaySfx(NA_SE_SY_LOCK_OFF); } } if (!(player->stateFlags1 & PLAYER_STATE1_2)) { Attention_Update(&actorCtx->attention, player, actor, play); } TitleCard_Update(&play->state, &actorCtx->titleCtx); Actor_UpdatePlayerImpact(play); DynaPoly_UpdateBgActorTransforms(play, &play->colCtx.dyna); } void Actor_Draw(PlayState* play, Actor* actor) { Lights* light; OPEN_DISPS(play->state.gfxCtx); light = LightContext_NewLights(&play->lightCtx, play->state.gfxCtx); if ((actor->flags & ACTOR_FLAG_UCODE_POINT_LIGHT_ENABLED) && (play->roomCtx.curRoom.enablePosLights || (MREG(93) != 0))) { light->enablePosLights = true; } Lights_BindAll(light, play->lightCtx.listHead, (actor->flags & (ACTOR_FLAG_UCODE_POINT_LIGHT_ENABLED | ACTOR_FLAG_IGNORE_LEGACY_POINT_LIGHTS)) ? NULL : &actor->world.pos, play); Lights_Draw(light, play->state.gfxCtx); if (actor->flags & ACTOR_FLAG_IGNORE_QUAKE) { Matrix_SetTranslateRotateYXZ(actor->world.pos.x + play->mainCamera.quakeOffset.x, actor->world.pos.y + ((actor->shape.yOffset * actor->scale.y) + play->mainCamera.quakeOffset.y), actor->world.pos.z + play->mainCamera.quakeOffset.z, &actor->shape.rot); } else { Matrix_SetTranslateRotateYXZ(actor->world.pos.x, actor->world.pos.y + (actor->shape.yOffset * actor->scale.y), actor->world.pos.z, &actor->shape.rot); } Matrix_Scale(actor->scale.x, actor->scale.y, actor->scale.z, MTXMODE_APPLY); Actor_SetObjectDependency(play, actor); gSPSegment(POLY_OPA_DISP++, 0x06, play->objectCtx.slots[actor->objectSlot].segment); gSPSegment(POLY_XLU_DISP++, 0x06, play->objectCtx.slots[actor->objectSlot].segment); if (actor->colorFilterTimer != 0) { s32 colorFlag = COLORFILTER_GET_COLORFLAG(actor->colorFilterParams); Color_RGBA8 actorDefaultHitColor = { 0, 0, 0, 255 }; if (colorFlag == COLORFILTER_COLORFLAG_GRAY) { actorDefaultHitColor.r = actorDefaultHitColor.g = actorDefaultHitColor.b = COLORFILTER_GET_COLORINTENSITY(actor->colorFilterParams) | 7; } else if (colorFlag == COLORFILTER_COLORFLAG_RED) { actorDefaultHitColor.r = COLORFILTER_GET_COLORINTENSITY(actor->colorFilterParams) | 7; } else if (colorFlag == COLORFILTER_COLORFLAG_NONE) { actorDefaultHitColor.b = actorDefaultHitColor.g = actorDefaultHitColor.r = 0; } else { actorDefaultHitColor.b = COLORFILTER_GET_COLORINTENSITY(actor->colorFilterParams) | 7; } if (actor->colorFilterParams & COLORFILTER_BUFFLAG_XLU) { func_800AE778(play, &actorDefaultHitColor, actor->colorFilterTimer, COLORFILTER_GET_DURATION(actor->colorFilterParams)); } else { func_800AE434(play, &actorDefaultHitColor, actor->colorFilterTimer, COLORFILTER_GET_DURATION(actor->colorFilterParams)); } } actor->draw(actor, play); if (actor->colorFilterTimer != 0) { if (actor->colorFilterParams & COLORFILTER_BUFFLAG_XLU) { func_800AE8EC(play); } else { func_800AE5A0(play); } } if (actor->shape.shadowDraw != NULL) { actor->shape.shadowDraw(actor, light, play); } actor->isDrawn = true; CLOSE_DISPS(play->state.gfxCtx); } void Actor_UpdateFlaggedAudio(Actor* actor) { s32 sfxId = actor->sfxId; if (sfxId != NA_SE_NONE) { if (actor->audioFlags & ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_2) { AudioSfx_PlaySfx(sfxId, &actor->projectedPos, 4, &gSfxDefaultFreqAndVolScale, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); } else if (actor->audioFlags & ACTOR_AUDIO_FLAG_SFX_CENTERED_1) { Audio_PlaySfx(sfxId); } else if (actor->audioFlags & ACTOR_AUDIO_FLAG_SFX_CENTERED_2) { Audio_PlaySfx_2(sfxId); } else if (actor->audioFlags & ACTOR_AUDIO_FLAG_SFX_TIMER) { Audio_PlaySfx_AtPosWithChannelIO(&gSfxDefaultPos, NA_SE_SY_TIMER - SFX_FLAG, (sfxId - 1)); } else if (actor->audioFlags & ACTOR_AUDIO_FLAG_SFX_ACTOR_POS_1) { Audio_PlaySfx_AtPos(&actor->projectedPos, sfxId); } } //! FAKE: if (sfxId != NA_SE_NONE) {} if (actor->audioFlags & ACTOR_AUDIO_FLAG_SEQ_MUSIC_BOX_HOUSE) { Audio_PlaySequenceAtPos(SEQ_PLAYER_BGM_SUB, &actor->projectedPos, NA_BGM_MUSIC_BOX_HOUSE, 1500.0f); } if (actor->audioFlags & ACTOR_AUDIO_FLAG_SEQ_KAMARO_DANCE) { Audio_PlaySequenceAtPos(SEQ_PLAYER_BGM_MAIN, &actor->projectedPos, NA_BGM_KAMARO_DANCE, 900.0f); } } void Actor_ResetLensActors(PlayState* play) { play->actorCtx.numLensActors = 0; play->actorCtx.lensActorsDrawn = false; } s32 Actor_AddToLensActors(PlayState* play, Actor* actor) { if (play->actorCtx.numLensActors >= LENS_ACTOR_MAX) { return false; } play->actorCtx.lensActors[play->actorCtx.numLensActors] = actor; play->actorCtx.numLensActors++; return true; } void Actor_DrawLensOverlay(Gfx** gfxP, s32 lensMaskSize) { TransitionCircle_LoadAndSetTexture(gfxP, gCircleTex, G_IM_FMT_I, 0, 6, 6, ((LENS_MASK_ACTIVE_SIZE - lensMaskSize) * 0.003f) + 1.0f); } void Actor_DrawLensActors(PlayState* play, s32 numLensActors, Actor** lensActors) { GraphicsContext* gfxCtx = play->state.gfxCtx; Actor** lensActor; s32 i; Gfx* gfx; Gfx* gfxTemp; void* zbuffer; void* spA4; s32 dbgVar1; s32 dbgVar2; s32 pad[2]; // Remnant of debug dbgVar1 = true; dbgVar2 = true; if (dbgVar1) { dbgVar1 = (numLensActors > 0); } OPEN_DISPS(gfxCtx); if (dbgVar1) { gfx = POLY_XLU_DISP; zbuffer = gfxCtx->zbuffer; spA4 = play->unk_18E68; if (dbgVar2) { PreRender_SetValues(&play->pauseBgPreRender, gCfbWidth, gCfbHeight, gfxCtx->curFrameBuffer, zbuffer); gfxTemp = gfx; func_80170200(&play->pauseBgPreRender, &gfxTemp, zbuffer, spA4); gfx = gfxTemp; } gDPPipeSync(gfx++); gDPSetPrimDepth(gfx++, 0, 0); gDPSetOtherMode(gfx++, G_AD_DISABLE | G_CD_MAGICSQ | G_CK_NONE | G_TC_FILT | G_TF_BILERP | G_TT_NONE | G_TL_TILE | G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE, G_AC_THRESHOLD | G_ZS_PRIM | Z_UPD | IM_RD | CVG_DST_SAVE | ZMODE_OPA | FORCE_BL | GBL_c1(G_BL_CLR_BL, G_BL_0, G_BL_CLR_MEM, G_BL_1MA) | GBL_c2(G_BL_CLR_BL, G_BL_0, G_BL_CLR_MEM, G_BL_1MA)); gDPSetPrimColor(gfx++, 0, 0, 0, 0, 0, 255); if (play->roomCtx.curRoom.lensMode == LENS_MODE_SHOW_ACTORS) { gDPSetCombineLERP(gfx++, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0); } else { gDPSetCombineMode(gfx++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); } gfxTemp = gfx; Actor_DrawLensOverlay(&gfxTemp, play->actorCtx.lensMaskSize); gfx = Play_SetFog(play, gfxTemp); for (i = 0, lensActor = lensActors; i < numLensActors; i++, lensActor++) { POLY_XLU_DISP = gfx; Actor_Draw(play, *lensActor); gfx = POLY_XLU_DISP; } if (dbgVar2) { gDPPipeSync(gfx++); gDPSetOtherMode(gfx++, G_AD_DISABLE | G_CD_DISABLE | G_CK_NONE | G_TC_FILT | G_TF_BILERP | G_TT_NONE | G_TL_TILE | G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE, G_AC_THRESHOLD | G_ZS_PRIM | AA_EN | IM_RD | CVG_DST_WRAP | ZMODE_OPA | CVG_X_ALPHA | ALPHA_CVG_SEL | FORCE_BL | GBL_c1(G_BL_CLR_IN, G_BL_0, G_BL_CLR_MEM, G_BL_1) | GBL_c2(G_BL_CLR_IN, G_BL_0, G_BL_CLR_MEM, G_BL_1)); gDPSetBlendColor(gfx++, 255, 255, 255, 0); gDPSetPrimColor(gfx++, 0, 0xFF, 0, 0, 0, 32); if (play->roomCtx.curRoom.lensMode == LENS_MODE_SHOW_ACTORS) { gDPSetCombineMode(gfx++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); } else { gDPSetCombineLERP(gfx++, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0); } gDPSetColorImage(gfx++, G_IM_FMT_RGBA, G_IM_SIZ_16b, play->pauseBgPreRender.width, spA4); gfxTemp = gfx; Actor_DrawLensOverlay(&gfxTemp, play->actorCtx.lensMaskSize); gfx = gfxTemp; gDPPipeSync(gfx++); gDPSetBlendColor(gfx++, 255, 255, 255, 8); gDPSetColorImage(gfx++, G_IM_FMT_RGBA, G_IM_SIZ_16b, play->pauseBgPreRender.width, play->pauseBgPreRender.fbuf); gfxTemp = gfx; PreRender_CopyImage(&play->pauseBgPreRender, &gfxTemp, spA4, zbuffer, true); gfx = gfxTemp; } POLY_XLU_DISP = gfx; } gfx = OVERLAY_DISP; gDPPipeSync(gfx++); gDPSetOtherMode(gfx++, G_AD_DISABLE | G_CD_MAGICSQ | G_CK_NONE | G_TC_FILT | G_TF_BILERP | G_TT_NONE | G_TL_TILE | G_TD_CLAMP | G_TP_NONE | G_CYC_1CYCLE | G_PM_NPRIMITIVE, G_AC_THRESHOLD | G_ZS_PRIM | G_RM_CLD_SURF | G_RM_CLD_SURF2); gDPSetCombineLERP(gfx++, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0, 1, TEXEL0, PRIMITIVE, 0); gDPSetPrimColor(gfx++, 0, 0, 74, 0, 0, 74); gfxTemp = gfx; Actor_DrawLensOverlay(&gfxTemp, play->actorCtx.lensMaskSize); OVERLAY_DISP = gfxTemp; CLOSE_DISPS(gfxCtx); } /** * Checks if an actor should be culled or not, by seeing if it is contained within its own culling volume. * For more details on the culling test, see `Actor_CullingVolumeTest`. * * Returns true if the actor is inside its culling volume. In other words, it should not cull. * * "Culling" in this context refers to the removal of something for the sake of improving performance. * For actors, being culled means that their Update and Draw processes are halted. * While halted, an Actor's update state is frozen and it will not draw, making it invisible. * * Actors that are within the bounds of their culling volume may update and draw, while actors that are * out of bounds of its culling volume may be excluded from updating and drawing until they are within bounds. * * It is possible for actors to opt out of update culling or draw culling. * This is set per-actor with `ACTOR_FLAG_UPDATE_CULLING_DISABLED` and `ACTOR_FLAG_DRAW_CULLING_DISABLED`. * * Note: Even if either `ACTOR_FLAG_UPDATE_CULLING_DISABLED` or `ACTOR_FLAG_DRAW_CULLING_DISABLED` are set, the actor * will still undergo the culling test and set `ACTOR_FLAG_INSIDE_CULLING_VOLUME` accordingly. * So, `ACTOR_FLAG_INSIDE_CULLING_VOLUME` cannot be used on it own to determine if an actor is actually culled. * It simply says whether or not they are physically located within the bounds of the culling volume. */ s32 Actor_CullingCheck(PlayState* play, Actor* actor) { return Actor_CullingVolumeTest(play, actor, &actor->projectedPos, actor->projectedW); } /** * Tests if an actor is currently within the bounds of its own culling volume. * * The culling volume is a 3D shape composed of a frustum with a box attached to the end of it. The frustum sits at the * camera's position and projects forward, encompassing the player's current view; the box extrudes behind the camera, * allowing actors in the immediate vicinity behind and to the sides of the camera to be detected. * * This function returns true if the actor is within bounds, false if not. * The comparison is done in projected space against the actor's projected position as the viewing frustum * in world space transforms to a box in projected space, making the calculation easy. * * Every actor can set properties for their own culling volume, changing its dimensions to suit the needs of * it and its environment. These properties are in units of projected space (i.e. compared to the actor's position * after perspective projection is applied) are therefore not directly comparable to world units. * These depend on the current view parameters (aspect, scale, znear, zfar). * The default parameters considered are (4/3, 1.0, 10, 12800). * * cullingVolumeDistance: Configures how far forward the far plane of the frustum should extend. * This along with cullingVolumeScale determines the maximum distance from * the camera eye that the actor can be detected at. This quantity is related * to world units by a factor of * (znear - zfar) / ((znear + zfar) * scale). * For default view parameters, increasing this property by 1 increases the * distance by ~0.995 world units. * * cullingVolumeScale: Scales the entire culling volume in all directions except the downward * direction. Both the frustum and the box will scale in size. This quantity is * related to world units by different factors based on direction: * - For the forward and backward directions, they are related in the same way * as above. For default view parameters, increasing this property by 1 increases * the forward and backward scales by ~0.995 world units. * - For the sideways directions, the relation to world units is * (aspect / scale) * sqrt(3)/3 * For default view parameters, increasing this property by 1 increases the * sideways scales by ~0.77 world units. * - For the upward direction, the relation to world units is * (1 / scale) * sqrt(3)/3 * For default view parameters, increasing this property by 1 increases the * scale by ~0.58 world units. * * cullingVolumeDownward: Sets the height of the culling volume in the downward direction. Increasing * this value will make actors below the camera more easily detected. This * quantity is related to world units by the same factor as the upward scale. * For default view parameters, increasing this property by 1 increases the * downward height by ~0.58 world units. * * This interactive 3D graph visualizes the shape of the culling volume and has sliders for the 3 properties mentioned * above: https://www.desmos.com/3d/4ztkxqky2a. */ s32 Actor_CullingVolumeTest(PlayState* play, Actor* actor, Vec3f* projPos, f32 projW) { if ((projPos->z > -actor->cullingVolumeScale) && (projPos->z < (actor->cullingVolumeDistance + actor->cullingVolumeScale))) { f32 invW; f32 cullingVolumeScaleX; f32 cullingVolumeScaleY; f32 cullingVolumeDownward; // Clamping `projW` affects points behind the camera, so that the culling volume has // a frustum shape in front of the camera and a box shape behind the camera. invW = CLAMP_MIN(projW, 1.0f); if (play->view.fovy != 60.0f) { // If the fov isn't 60 degrees, make the cull parameters behave as if it were 60 degrees. // To do this, multiply by the ratios of the x and y diagonal elements of the projection matrix. // The x diagonal element is cot(0.5 * fov) / aspect and the y diagonal element is just cot(0.5 * fov). // When the fov is 60 degrees, cot(0.5 * 60 degrees) = sqrt(3) so the x element is 3sqrt(3)/4 and the y // element is sqrt(3). The current diagonal element divided by (or multiplied by their inverse) gives // the ratio. cullingVolumeScaleX = actor->cullingVolumeScale * play->projectionMtxFDiagonal.x * 0.76980036f; // sqrt(16/27) = aspect / cot(0.5 * f) = (4/3) / sqrt(3) cullingVolumeDownward = play->projectionMtxFDiagonal.y * 0.57735026f; // 1 / sqrt(3) = 1 / cot(0.5 * f) cullingVolumeScaleY = actor->cullingVolumeScale * cullingVolumeDownward; cullingVolumeDownward *= actor->cullingVolumeDownward; } else { cullingVolumeScaleY = cullingVolumeScaleX = actor->cullingVolumeScale; cullingVolumeDownward = actor->cullingVolumeDownward; } if (((fabsf(projPos->x) - cullingVolumeScaleX) < invW) && ((-invW < (projPos->y + cullingVolumeDownward))) && ((projPos->y - cullingVolumeScaleY) < invW)) { return true; } } return false; } void Actor_DrawAll(PlayState* play, ActorContext* actorCtx) { s32 pad[2]; Gfx* ref2; Gfx* tmp2; s32 pad2; Gfx* sp58; ActorListEntry* actorEntry; Actor* actor; s32 drawActorFlagsMask; s32 category; if (play->soaringCsOrSoTCsPlaying) { drawActorFlagsMask = ACTOR_FLAG_UPDATE_DURING_SOARING_AND_SOT_CS; } else { drawActorFlagsMask = ACTOR_FLAG_UPDATE_DURING_SOARING_AND_SOT_CS | ACTOR_FLAG_INSIDE_CULLING_VOLUME | ACTOR_FLAG_DRAW_CULLING_DISABLED; } OPEN_DISPS(play->state.gfxCtx); Actor_ResetLensActors(play); sp58 = POLY_XLU_DISP; POLY_XLU_DISP = &sp58[1]; for (category = 0, actorEntry = actorCtx->actorLists; category < ACTORCAT_MAX; category++, actorEntry++) { actor = actorEntry->first; while (actor != NULL) { SkinMatrix_Vec3fMtxFMultXYZW(&play->viewProjectionMtxF, &actor->world.pos, &actor->projectedPos, &actor->projectedW); if (actor->audioFlags & ACTOR_AUDIO_FLAG_ALL) { Actor_UpdateFlaggedAudio(actor); } if (Actor_CullingCheck(play, actor)) { actor->flags |= ACTOR_FLAG_INSIDE_CULLING_VOLUME; } else { actor->flags &= ~ACTOR_FLAG_INSIDE_CULLING_VOLUME; } actor->isDrawn = false; if ((actor->init == NULL) && (actor->draw != NULL) && (actor->flags & drawActorFlagsMask)) { if ((actor->flags & ACTOR_FLAG_REACT_TO_LENS) && ((play->roomCtx.curRoom.lensMode == LENS_MODE_SHOW_ACTORS) || (play->actorCtx.lensMaskSize == LENS_MASK_ACTIVE_SIZE) || (actor->room != play->roomCtx.curRoom.num))) { if (Actor_AddToLensActors(play, actor)) {} } else { Actor_Draw(play, actor); } } actor = actor->next; } } Effect_DrawAll(play->state.gfxCtx); EffectSS_DrawAllParticles(play); EffFootmark_Draw(play); ref2 = POLY_XLU_DISP; gSPDisplayList(sp58, &ref2[1]); POLY_XLU_DISP = &ref2[1]; if (play->actorCtx.lensActive) { Math_StepToC(&play->actorCtx.lensMaskSize, LENS_MASK_ACTIVE_SIZE, 20); if (GET_PLAYER(play)->stateFlags2 & PLAYER_STATE2_USING_OCARINA) { Actor_DeactivateLens(play); } } else { Math_StepToC(&play->actorCtx.lensMaskSize, 0, 10); } if (play->actorCtx.lensMaskSize != 0) { play->actorCtx.lensActorsDrawn = true; Actor_DrawLensActors(play, play->actorCtx.numLensActors, play->actorCtx.lensActors); } tmp2 = POLY_XLU_DISP; gSPEndDisplayList(&tmp2[0]); gSPBranchList(ref2, &tmp2[1]); POLY_XLU_DISP = &tmp2[1]; if (!play->soaringCsOrSoTCsPlaying) { Lights_DrawGlow(play); } TitleCard_Draw(&play->state, &actorCtx->titleCtx); CLOSE_DISPS(play->state.gfxCtx); } /** * Kill every actor which depends on an object that is not loaded. */ void Actor_KillAllWithMissingObject(PlayState* play, ActorContext* actorCtx) { Actor* actor; s32 category; for (category = 0; category < ACTORCAT_MAX; category++) { actor = actorCtx->actorLists[category].first; while (actor != NULL) { if (!Object_IsLoaded(&play->objectCtx, actor->objectSlot)) { Actor_Kill(actor); } actor = actor->next; } } } /** * Kill actors on room change and update flags accordingly */ void func_800BA798(PlayState* play, ActorContext* actorCtx) { Actor* actor; s32 category; for (category = 0; category < ACTORCAT_MAX; category++) { actor = actorCtx->actorLists[category].first; while (actor != NULL) { if ((actor->room >= 0) && (actor->room != play->roomCtx.curRoom.num) && (actor->room != play->roomCtx.prevRoom.num)) { if (!actor->isDrawn) { actor = Actor_Delete(actorCtx, actor, play); } else { Actor_Kill(actor); Actor_Destroy(actor, play); actor = actor->next; } } else { actor = actor->next; } } } CollisionCheck_ClearContext(play, &play->colChkCtx); actorCtx->sceneFlags.clearedRoomTemp = 0; actorCtx->sceneFlags.switches[3] = 0; actorCtx->sceneFlags.collectible[3] = 0; play->msgCtx.unk_12030 = 0; } /** * Kill every actor which does not have the current halfDayBit enabled */ void Actor_KillAllOnHalfDayChange(PlayState* play, ActorContext* actorCtx) { s32 category; for (category = 0; category < ACTORCAT_MAX; category++) { Actor* actor = actorCtx->actorLists[category].first; while (actor != NULL) { if (!(actor->halfDaysBits & actorCtx->halfDaysBit)) { func_80123590(play, actor); if (!actor->isDrawn) { actor = Actor_Delete(actorCtx, actor, play); } else { Actor_Kill(actor); Actor_Destroy(actor, play); actor = actor->next; } } else { actor = actor->next; } } } CollisionCheck_ClearContext(play, &play->colChkCtx); play->msgCtx.unk_12030 = 0; } void Actor_CleanupContext(ActorContext* actorCtx, PlayState* play) { s32 category; Fault_RemoveClient(&sActorFaultClient); for (category = 0; category < ACTORCAT_MAX; category++) { if (category != ACTORCAT_PLAYER) { Actor* actor = actorCtx->actorLists[category].first; while (actor != NULL) { Actor_Delete(actorCtx, actor, play); actor = actorCtx->actorLists[category].first; } } } while (actorCtx->actorLists[ACTORCAT_PLAYER].first != NULL) { Actor_Delete(actorCtx, actorCtx->actorLists[ACTORCAT_PLAYER].first, play); } if (actorCtx->absoluteSpace != NULL) { ZeldaArena_Free(actorCtx->absoluteSpace); actorCtx->absoluteSpace = NULL; } Play_SaveCycleSceneFlags(play); ActorOverlayTable_Cleanup(); } /** * Adds a given actor instance at the front of the actor list of the specified category. * Also sets the actor instance as being of that category. */ void Actor_AddToCategory(ActorContext* actorCtx, Actor* actor, u8 actorCategory) { Actor* actorAux; Actor* lastActor; actor->category = actorCategory; actorCtx->totalLoadedActors++; actorCtx->actorLists[actorCategory].length++; lastActor = actorCtx->actorLists[actorCategory].first; if (lastActor == NULL) { actorCtx->actorLists[actorCategory].first = actor; return; } actorAux = lastActor->next; while (actorAux != NULL) { lastActor = actorAux; actorAux = actorAux->next; } lastActor->next = actor; actor->prev = lastActor; } /** * Removes a given actor instance from its actor list. * Also sets the temp clear flag of the current room if the actor removed was the last enemy loaded. */ Actor* Actor_RemoveFromCategory(PlayState* play, ActorContext* actorCtx, Actor* actorToRemove) { Actor* newHead; actorCtx->totalLoadedActors--; actorCtx->actorLists[actorToRemove->category].length--; if (actorToRemove->prev != NULL) { actorToRemove->prev->next = actorToRemove->next; } else { actorCtx->actorLists[actorToRemove->category].first = actorToRemove->next; } newHead = actorToRemove->next; if (newHead != NULL) { newHead->prev = actorToRemove->prev; } actorToRemove->next = NULL; actorToRemove->prev = NULL; if ((actorToRemove->room == play->roomCtx.curRoom.num) && (actorToRemove->category == ACTORCAT_ENEMY) && (actorCtx->actorLists[ACTORCAT_ENEMY].length == 0)) { Flags_SetClearTemp(play, play->roomCtx.curRoom.num); } return newHead; } void Actor_FreeOverlay(ActorOverlay* entry) { if (entry->numLoaded == 0) { void* ramAddr = entry->loadedRamAddr; if (ramAddr != NULL) { if (!(entry->allocType & ALLOCTYPE_PERMANENT)) { if (entry->allocType & ALLOCTYPE_ABSOLUTE) { entry->loadedRamAddr = NULL; } else { ZeldaArena_Free(ramAddr); entry->loadedRamAddr = NULL; } } } } } Actor* Actor_Spawn(ActorContext* actorCtx, PlayState* play, s16 actorId, f32 posX, f32 posY, f32 posZ, s16 rotX, s16 rotY, s16 rotZ, s32 params) { return Actor_SpawnAsChildAndCutscene(actorCtx, play, actorId, posX, posY, posZ, rotX, rotY, rotZ, params, CS_ID_NONE, HALFDAYBIT_ALL, NULL); } ActorProfile* Actor_LoadOverlay(ActorContext* actorCtx, s16 index) { size_t overlaySize; ActorOverlay* overlayEntry = &gActorOverlayTable[index]; ActorProfile* profile; overlaySize = (uintptr_t)overlayEntry->vramEnd - (uintptr_t)overlayEntry->vramStart; if (overlayEntry->vramStart == NULL) { profile = overlayEntry->profile; } else { if (overlayEntry->loadedRamAddr == NULL) { if (overlayEntry->allocType & ALLOCTYPE_ABSOLUTE) { if (actorCtx->absoluteSpace == NULL) { actorCtx->absoluteSpace = ZeldaArena_MallocR(AM_FIELD_SIZE); } gActorOverlayTable[index].loadedRamAddr = actorCtx->absoluteSpace; } else if (overlayEntry->allocType & ALLOCTYPE_PERMANENT) { gActorOverlayTable[index].loadedRamAddr = ZeldaArena_MallocR(overlaySize); } else { gActorOverlayTable[index].loadedRamAddr = ZeldaArena_Malloc(overlaySize); } if (overlayEntry->loadedRamAddr == NULL) { return NULL; } Overlay_Load(overlayEntry->file.vromStart, overlayEntry->file.vromEnd, overlayEntry->vramStart, overlayEntry->vramEnd, overlayEntry->loadedRamAddr); overlayEntry->numLoaded = 0; } profile = (void*)(uintptr_t)((overlayEntry->profile != NULL) ? (void*)((uintptr_t)overlayEntry->profile - (intptr_t)((uintptr_t)overlayEntry->vramStart - (uintptr_t)overlayEntry->loadedRamAddr)) : NULL); } return profile; } Actor* Actor_SpawnAsChildAndCutscene(ActorContext* actorCtx, PlayState* play, s16 index, f32 x, f32 y, f32 z, s16 rotX, s16 rotY, s16 rotZ, s32 params, u32 csId, u32 halfDaysBits, Actor* parent) { s32 pad; Actor* actor; ActorProfile* profile; s32 objectSlot; ActorOverlay* overlayEntry; if (actorCtx->totalLoadedActors >= 255) { return NULL; } profile = Actor_LoadOverlay(actorCtx, index); if (profile == NULL) { return NULL; } objectSlot = Object_GetSlot(&play->objectCtx, profile->objectId); if ((objectSlot <= OBJECT_SLOT_NONE) || ((profile->type == ACTORCAT_ENEMY) && Flags_GetClear(play, play->roomCtx.curRoom.num) && (profile->id != ACTOR_BOSS_05))) { Actor_FreeOverlay(&gActorOverlayTable[index]); return NULL; } actor = ZeldaArena_Malloc(profile->instanceSize); if (actor == NULL) { Actor_FreeOverlay(&gActorOverlayTable[index]); return NULL; } overlayEntry = &gActorOverlayTable[index]; if (overlayEntry->vramStart != NULL) { overlayEntry->numLoaded++; } bzero(actor, profile->instanceSize); actor->overlayEntry = overlayEntry; actor->id = profile->id; actor->flags = profile->flags; if (profile->id == ACTOR_EN_PART) { actor->objectSlot = rotZ; rotZ = 0; } else { actor->objectSlot = objectSlot; } actor->init = profile->init; actor->destroy = profile->destroy; actor->update = profile->update; actor->draw = profile->draw; if (parent != NULL) { actor->room = parent->room; actor->parent = parent; parent->child = actor; } else { actor->room = play->roomCtx.curRoom.num; } actor->home.pos.x = x; actor->home.pos.y = y; actor->home.pos.z = z; actor->home.rot.x = rotX; actor->home.rot.y = rotY; actor->home.rot.z = rotZ; actor->params = params & 0xFFFF; actor->csId = csId & 0x7F; if (actor->csId == 0x7F) { actor->csId = CS_ID_NONE; } if (halfDaysBits != 0) { actor->halfDaysBits = halfDaysBits; } else { actor->halfDaysBits = HALFDAYBIT_ALL; } Actor_AddToCategory(actorCtx, actor, profile->type); { uintptr_t prevSeg = gSegments[0x06]; Actor_Init(actor, play); gSegments[0x06] = prevSeg; } return actor; } Actor* Actor_SpawnAsChild(ActorContext* actorCtx, Actor* parent, PlayState* play, s16 actorId, f32 posX, f32 posY, f32 posZ, s16 rotX, s16 rotY, s16 rotZ, s32 params) { return Actor_SpawnAsChildAndCutscene(actorCtx, play, actorId, posX, posY, posZ, rotX, rotY, rotZ, params, CS_ID_NONE, parent->halfDaysBits, parent); } void Actor_SpawnTransitionActors(PlayState* play, ActorContext* actorCtx) { TransitionActorEntry* transitionActorList = play->transitionActors.list; s32 i; s16 numTransitionActors = play->transitionActors.count; for (i = 0; i < numTransitionActors; transitionActorList++, i++) { if (transitionActorList->id >= 0) { if (((transitionActorList->sides[0].room >= 0) && ((play->roomCtx.curRoom.num == transitionActorList->sides[0].room) || (play->roomCtx.prevRoom.num == transitionActorList->sides[0].room))) || ((transitionActorList->sides[1].room >= 0) && ((play->roomCtx.curRoom.num == transitionActorList->sides[1].room) || (play->roomCtx.prevRoom.num == transitionActorList->sides[1].room)))) { s16 rotY = DEG_TO_BINANG((transitionActorList->rotY >> 7) & 0x1FF); if (Actor_SpawnAsChildAndCutscene(actorCtx, play, transitionActorList->id & 0x1FFF, transitionActorList->pos.x, transitionActorList->pos.y, transitionActorList->pos.z, 0, rotY, 0, TRANSITION_ACTOR_PARAMS(i, transitionActorList->params), transitionActorList->rotY & 0x7F, HALFDAYBIT_ALL, 0) != NULL) { transitionActorList->id = -transitionActorList->id; } numTransitionActors = play->transitionActors.count; } } } } Actor* Actor_SpawnEntry(ActorContext* actorCtx, ActorEntry* actorEntry, PlayState* play) { s16 rotX = (actorEntry->rot.x >> 7) & 0x1FF; s16 rotY = (actorEntry->rot.y >> 7) & 0x1FF; s16 rotZ = (actorEntry->rot.z >> 7) & 0x1FF; if (!(actorEntry->id & 0x8000)) { rotY = DEG_TO_BINANG(rotY); } else if (rotY > 180) { rotY -= 360; } if (!(actorEntry->id & 0x4000)) { rotX = DEG_TO_BINANG(rotX); } else if (rotX > 180) { rotX -= 360; } if (!(actorEntry->id & 0x2000)) { rotZ = DEG_TO_BINANG(rotZ); } else if (rotZ > 180) { rotZ -= 360; } return Actor_SpawnAsChildAndCutscene(actorCtx, play, actorEntry->id & 0x1FFF, actorEntry->pos.x, actorEntry->pos.y, actorEntry->pos.z, rotX, rotY, rotZ, actorEntry->params & 0xFFFF, actorEntry->rot.y & 0x7F, ((actorEntry->rot.x & 7) << 7) | (actorEntry->rot.z & 0x7F), NULL); } Actor* Actor_Delete(ActorContext* actorCtx, Actor* actor, PlayState* play) { s32 pad; Player* player = GET_PLAYER(play); Actor* newHead; ActorOverlay* overlayEntry = actor->overlayEntry; if ((player != NULL) && (actor == player->focusActor)) { Player_ReleaseLockOn(player); Camera_ChangeMode(Play_GetCamera(play, Play_GetActiveCamId(play)), CAM_MODE_NORMAL); } if (actor == actorCtx->attention.tatlHoverActor) { actorCtx->attention.tatlHoverActor = NULL; } if (actor == actorCtx->attention.forcedLockOnActor) { actorCtx->attention.forcedLockOnActor = NULL; } if (actor == actorCtx->attention.bgmEnemy) { actorCtx->attention.bgmEnemy = NULL; } AudioSfx_StopByPos(&actor->projectedPos); Actor_Destroy(actor, play); newHead = Actor_RemoveFromCategory(play, actorCtx, actor); ZeldaArena_Free(actor); if (overlayEntry->vramStart != NULL) { overlayEntry->numLoaded--; Actor_FreeOverlay(overlayEntry); } return newHead; } /** * Checks that an actor is on-screen enough to be considered an attention actor. * * Note that the screen bounds checks are larger than the actual screen region * to give room for error. */ bool Attention_ActorOnScreen(PlayState* play, Actor* actor) { s16 x; s16 y; Actor_GetScreenPos(play, actor, &x, &y); #define X_LEEWAY 20 #define Y_LEEWAY 160 return (x > (0 - X_LEEWAY)) && (x < (gScreenWidth + X_LEEWAY)) && (y > (0 - Y_LEEWAY)) && (y < (gScreenHeight + Y_LEEWAY)); } /** * Search for attention actors or camera drift actors within the specified category. * * To be considered an attention actor the actor needs to: * - Have a non-NULL update function (still active) * - Not be player (this is technically a redundant check because the PLAYER category is never searched) * - Have `ACTOR_FLAG_ATTENTION_ENABLED` or `ACTOR_FLAG_FOCUS_ACTOR_REFINDABLE` set * - Not be the current focus actor unless `ACTOR_FLAG_FOCUS_ACTOR_REFINDABLE` is set * - Be the closest attention actor found so far * - Be within range, specified by attentionRangeType * - Be roughly on-screen * - Not be blocked by a surface * * If an actor has a priority value set and the value is the lowest found so far, it will be set as the prioritized * attention actor. Otherwise, it is set as the nearest attention actor or camera drift actor. * * This function is expected to be called with almost every actor category in each cycle. On a new cycle its global * variables must be reset by the caller, otherwise the information of the previous cycle will be retained. */ void Attention_FindActorInCategory(PlayState* play, ActorContext* actorCtx, Player* player, ActorType actorCategory) { f32 distSq; Actor* actor = actorCtx->actorLists[actorCategory].first; Actor* playerFocusActor = player->focusActor; s32 isNearestAttentionActor; s32 isNearestCameraDriftActor; for (; actor != NULL; actor = actor->next) { if ((actor->update == NULL) || ((Player*)actor == player)) { continue; } if (!(actor->flags & (ACTOR_FLAG_ATTENTION_ENABLED | ACTOR_FLAG_CAMERA_DRIFT_ENABLED))) { continue; } // Determine the closest enemy actor to player within a range. Used for playing enemy background music. if ((actorCategory == ACTORCAT_ENEMY) && CHECK_FLAG_ALL(actor->flags, ACTOR_FLAG_ATTENTION_ENABLED | ACTOR_FLAG_HOSTILE)) { if ((actor->xyzDistToPlayerSq < SQ(500.0f)) && (actor->xyzDistToPlayerSq < sBgmEnemyDistSq)) { actorCtx->attention.bgmEnemy = actor; sBgmEnemyDistSq = actor->xyzDistToPlayerSq; } } // Ignore the current focus actor unless it is refindable if ((actor == playerFocusActor) && !(actor->flags & ACTOR_FLAG_FOCUS_ACTOR_REFINDABLE)) { continue; } distSq = Attention_WeightedDistToPlayerSq(actor, player, sAttentionPlayerRotY); isNearestAttentionActor = (actor->flags & ACTOR_FLAG_ATTENTION_ENABLED) && (distSq < sNearestAttentionActorDistSq); isNearestCameraDriftActor = (actor->flags & ACTOR_FLAG_CAMERA_DRIFT_ENABLED) && (distSq < sNearestCameraDriftActorDistSq); if (!isNearestAttentionActor && !isNearestCameraDriftActor) { continue; } if (Attention_ActorIsInRange(actor, distSq) && Attention_ActorOnScreen(play, actor)) { CollisionPoly* poly; s32 bgId; Vec3f lineTestResultPos; if (BgCheck_CameraLineTest1(&play->colCtx, &player->actor.focus.pos, &actor->focus.pos, &lineTestResultPos, &poly, true, true, true, true, &bgId)) { if (!SurfaceType_IsIgnoredByProjectiles(&play->colCtx, poly, bgId)) { continue; } } if (actor->targetPriority != 0) { if (isNearestAttentionActor && (actor->targetPriority < sHighestAttentionPriority)) { sPrioritizedAttentionActor = actor; sHighestAttentionPriority = actor->targetPriority; } if (isNearestCameraDriftActor && (actor->targetPriority < sHighestCameraDriftPriority)) { sPrioritizedCameraDriftActor = actor; sHighestCameraDriftPriority = actor->targetPriority; } } else { if (isNearestAttentionActor) { sNearestAttentionActor = actor; sNearestAttentionActorDistSq = distSq; } if (isNearestCameraDriftActor) { sNearestCameraDriftActor = actor; sNearestCameraDriftActorDistSq = distSq; } } } } } u8 sAttentionCategorySearchOrder[] = { ACTORCAT_BOSS, ACTORCAT_ENEMY, ACTORCAT_BG, ACTORCAT_EXPLOSIVES, ACTORCAT_NPC, ACTORCAT_ITEMACTION, ACTORCAT_CHEST, ACTORCAT_SWITCH, ACTORCAT_PROP, ACTORCAT_MISC, ACTORCAT_DOOR, ACTORCAT_SWITCH, }; /** * Search for the nearest attention actor and camera drift actor by iterating through most actor categories. * See `Attention_FindActorInCategory` for more details on search criteria. * * The attention actor found is stored in the `attentionActorP` parameter, while the camera drift actor is stored in * `cameraDriftActorP` They may be NULL if no actor that fulfills the criteria is found. */ void Attention_FindActor(PlayState* play, ActorContext* actorCtx, Actor** attentionActorP, Actor** cameraDriftActorP, Player* player) { u8* category; s32 i; sNearestAttentionActor = sPrioritizedAttentionActor = sNearestCameraDriftActor = sPrioritizedCameraDriftActor = NULL; sNearestAttentionActorDistSq = sNearestCameraDriftActorDistSq = sBgmEnemyDistSq = FLT_MAX; sHighestAttentionPriority = sHighestCameraDriftPriority = INT32_MAX; actorCtx->attention.bgmEnemy = NULL; sAttentionPlayerRotY = player->actor.shape.rot.y; category = sAttentionCategorySearchOrder; // Search the first 3 actor categories first for an attention actor // These are Boss, Enemy, and Bg, in order. for (i = 0; i < 3; i++) { Attention_FindActorInCategory(play, actorCtx, player, *category); category++; } // If no actor in the above categories was found, then try searching in the remaining categories if (sNearestAttentionActor == NULL) { for (; i < ARRAY_COUNT(sAttentionCategorySearchOrder); i++) { Attention_FindActorInCategory(play, actorCtx, player, *category); category++; } } if (sNearestAttentionActor == NULL) { *attentionActorP = sPrioritizedAttentionActor; } else { *attentionActorP = sNearestAttentionActor; } if (sNearestCameraDriftActor == NULL) { *cameraDriftActorP = sPrioritizedCameraDriftActor; } else { *cameraDriftActorP = sNearestCameraDriftActor; } } /** * Play the death sound effect and flash the screen white for 4 frames. * While the screen flashes, the game freezes. */ void Enemy_StartFinishingBlow(PlayState* play, Actor* actor) { play->actorCtx.freezeFlashTimer = 5; SoundSource_PlaySfxAtFixedWorldPos(play, &actor->world.pos, 20, NA_SE_EN_LAST_DAMAGE); } // blinking routine s16 func_800BBAC0(BlinkInfo* info, s16 arg1, s16 arg2, s16 arg3) { if (DECR(info->blinkTimer) == 0) { info->blinkTimer = Rand_S16Offset(arg1, arg2); } if (info->blinkTimer - arg3 > 0) { info->eyeTexIndex = 0; } else if ((info->blinkTimer - arg3 >= -1) || (info->blinkTimer < 2)) { info->eyeTexIndex = 1; } else { info->eyeTexIndex = 2; } return info->eyeTexIndex; } // blinking routine s16 func_800BBB74(BlinkInfo* info, s16 arg1, s16 arg2, s16 arg3) { if (DECR(info->blinkTimer) == 0) { info->blinkTimer = Rand_S16Offset(arg1, arg2); } if (info->blinkTimer - arg3 > 0) { info->eyeTexIndex = 0; } else if (info->blinkTimer - arg3 == 0) { info->eyeTexIndex = 1; } else { info->eyeTexIndex = 2; } return info->eyeTexIndex; } // unused blinking routine s16 func_800BBC20(BlinkInfo* info, s16 arg1, s16 arg2, s16 arg3) { if (DECR(info->blinkTimer) == 0) { info->blinkTimer = Rand_S16Offset(arg1, arg2); info->eyeTexIndex++; if ((info->eyeTexIndex % 3) == 0) { info->eyeTexIndex = (s32)(Rand_ZeroOne() * arg3) * 3; } } return info->eyeTexIndex; } void Actor_SpawnBodyParts(Actor* actor, PlayState* play, s32 partParams, Gfx** dList) { if (*dList != NULL) { EnPart* part; Actor* spawnedPart; MtxF* currentMatrix = Matrix_GetCurrent(); spawnedPart = Actor_SpawnAsChild(&play->actorCtx, actor, play, ACTOR_EN_PART, currentMatrix->mf[3][0], currentMatrix->mf[3][1], currentMatrix->mf[3][2], 0, 0, actor->objectSlot, partParams); if (spawnedPart != NULL) { part = (EnPart*)spawnedPart; Matrix_MtxFToYXZRot(currentMatrix, &part->actor.shape.rot, false); part->dList = *dList; Math_Vec3f_Copy(&part->actor.scale, &actor->scale); } } } void Actor_SpawnFloorDustRing(PlayState* play, Actor* actor, Vec3f* posXZ, f32 radius, s32 countMinusOne, f32 randAccelWeight, s16 scale, s16 scaleStep, u8 useLighting) { Vec3f pos; Vec3f accel = { 0.0f, 0.3f, 0.0f }; s32 pad[2]; f32 angle; s32 i; angle = (Rand_ZeroOne() - 0.5f) * (2.0f * 3.14f); pos.y = actor->floorHeight; accel.y += (Rand_ZeroOne() - 0.5f) * 0.2f; for (i = countMinusOne; i >= 0; i--) { pos.x = (Math_SinF(angle) * radius) + posXZ->x; pos.z = (Math_CosF(angle) * radius) + posXZ->z; accel.x = (Rand_ZeroOne() - 0.5f) * randAccelWeight; accel.z = (Rand_ZeroOne() - 0.5f) * randAccelWeight; if (scale == 0) { func_800B10C0(play, &pos, &gZeroVec3f, &accel); } else if (useLighting) { func_800B1210(play, &pos, &gZeroVec3f, &accel, scale, scaleStep); } else { func_800B11A0(play, &pos, &gZeroVec3f, &accel, scale, scaleStep); } angle += (2.0f * 3.14f) / (countMinusOne + 1.0f); } } void func_800BBFB0(PlayState* play, Vec3f* position, f32 arg2, s32 arg3, s16 arg4, s16 scaleStep, u8 arg6) { Vec3f pos; Vec3f accel = { 0.0f, 0.3f, 0.0f }; s32 i; for (i = arg3; i >= 0; i--) { s16 scale; pos.x = ((Rand_ZeroOne() - 0.5f) * arg2) + position->x; pos.y = ((Rand_ZeroOne() - 0.5f) * arg2) + position->y; pos.z = ((Rand_ZeroOne() - 0.5f) * arg2) + position->z; scale = (s32)(Rand_ZeroOne() * arg4 * 0.2f); scale += arg4; if (arg6) { func_800B1210(play, &pos, &gZeroVec3f, &accel, scale, scaleStep); } else { func_800B11A0(play, &pos, &gZeroVec3f, &accel, scale, scaleStep); } } } void Actor_ChangeCategory(PlayState* play, ActorContext* actorCtx, Actor* actor, u8 actorCategory) { actorCtx->actorLists[actor->category].categoryChanged = true; actor->category = actorCategory; } // Damage flags for EnArrow u32 sArrowDmgFlags[] = { DMG_FIRE_ARROW, // ARROW_TYPE_NORMAL_LIT DMG_NORMAL_ARROW, // ARROW_TYPE_NORMAL_HORSE DMG_NORMAL_ARROW, // ARROW_TYPE_NORMAL DMG_FIRE_ARROW, // ARROW_TYPE_FIRE DMG_ICE_ARROW, // ARROW_TYPE_ICE DMG_LIGHT_ARROW, // ARROW_TYPE_LIGHT DMG_DEKU_NUT, // ARROW_TYPE_SLINGSHOT DMG_DEKU_BUBBLE, // ARROW_TYPE_DEKU_BUBBLE DMG_DEKU_NUT, // ARROW_TYPE_DEKU_NUT }; u32 Actor_GetArrowDmgFlags(s32 params) { if ((params < 0) || (params >= ARRAY_COUNT(sArrowDmgFlags))) { return 0; } return sArrowDmgFlags[params]; } s32 func_800BC1B4(Actor* actor, Actor* projectile, f32 distance, f32 speed) { if ((speed > 0.0f) && (Actor_WorldDistXYZToActor(projectile, actor) < ((speed * 2.5f) + distance))) { s16 temp_v1 = BINANG_SUB(Actor_WorldYawTowardActor(projectile, actor), projectile->world.rot.y); if (ABS_ALT(temp_v1) < 0x1400) { return true; } } return false; } Actor* func_800BC270(PlayState* play, Actor* actor, f32 distance, u32 dmgFlags) { Actor* itemAction = play->actorCtx.actorLists[ACTORCAT_ITEMACTION].first; while (itemAction != NULL) { if (((itemAction->id == ACTOR_ARMS_HOOK) && (dmgFlags & DMG_HOOKSHOT)) || ((itemAction->id == ACTOR_EN_BOOM) && (dmgFlags & DMG_ZORA_BOOMERANG)) || ((itemAction->id == ACTOR_EN_ARROW) && (Actor_GetArrowDmgFlags(itemAction->params) & dmgFlags))) { f32 speed; if ((itemAction->speed <= 0.0f) && (GET_PLAYER(play)->unk_D57 != 0)) { if (itemAction->id == ACTOR_ARMS_HOOK) { speed = 20.0f; } else if (itemAction->id == ACTOR_EN_BOOM) { speed = 12.0f; } else { u32 arrowDmgFlags = Actor_GetArrowDmgFlags(itemAction->params); if (arrowDmgFlags == DMG_DEKU_NUT) { speed = 80.0f; } else if (arrowDmgFlags == DMG_DEKU_BUBBLE) { speed = 60.0f; } else { speed = 150.0f; } } } else { speed = itemAction->speed; } if (func_800BC1B4(actor, itemAction, distance, speed)) { break; } } itemAction = itemAction->next; } return itemAction; } Actor* func_800BC444(PlayState* play, Actor* actor, f32 distance) { Actor* explosive = play->actorCtx.actorLists[ACTORCAT_EXPLOSIVES].first; while (explosive != NULL) { if (((explosive->id == ACTOR_EN_BOM) || (explosive->id == ACTOR_EN_BOM_CHU) || (explosive->id == ACTOR_EN_BOMBF))) { if (func_800BC1B4(actor, explosive, distance, explosive->speed)) { break; } } explosive = explosive->next; } return explosive; } /** * Checks if a given actor will be standing on the ground after being translated * by the provided distance and angle. * * Returns true if the actor will be standing on ground. */ s16 Actor_TestFloorInDirection(Actor* actor, PlayState* play, f32 distance, s16 angle) { s16 ret; u16 bgCheckFlags; f32 dx; f32 dz; Vec3f actorPos; Math_Vec3f_Copy(&actorPos, &actor->world.pos); bgCheckFlags = actor->bgCheckFlags; dx = Math_SinS(angle) * distance; dz = Math_CosS(angle) * distance; actor->world.pos.x += dx; actor->world.pos.z += dz; Actor_UpdateBgCheckInfo(play, actor, 0.0f, 0.0f, 0.0f, UPDBGCHECKINFO_FLAG_4); Math_Vec3f_Copy(&actor->world.pos, &actorPos); ret = actor->bgCheckFlags & BGCHECKFLAG_GROUND; actor->bgCheckFlags = bgCheckFlags; return ret; } /** * Returns true if the player is targeting the provided actor */ s32 Actor_IsTargeted(PlayState* play, Actor* actor) { Player* player = GET_PLAYER(play); if ((player->stateFlags3 & PLAYER_STATE3_HOSTILE_LOCK_ON) && actor->isLockedOn) { return true; } return false; } /** * Returns true if the player is targeting an actor other than the provided actor */ s32 Actor_OtherIsTargeted(PlayState* play, Actor* actor) { Player* player = GET_PLAYER(play); if ((player->stateFlags3 & PLAYER_STATE3_HOSTILE_LOCK_ON) && !actor->isLockedOn) { return true; } return false; } void func_800BC620(Vec3f* pos, Vec3f* scale, u8 alpha, PlayState* play) { MtxF mf; f32 yIntersect; Vec3f adjustedPos; CollisionPoly* poly; OPEN_DISPS(play->state.gfxCtx); POLY_OPA_DISP = Gfx_SetupDL(POLY_OPA_DISP, SETUPDL_44); gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 0, 0, alpha); adjustedPos.x = pos->x; adjustedPos.y = pos->y + 1.0f; adjustedPos.z = pos->z; yIntersect = BgCheck_EntityRaycastFloor2(play, &play->colCtx, &poly, &adjustedPos); if (poly != NULL) { func_800C0094(poly, pos->x, yIntersect, pos->z, &mf); Matrix_Put(&mf); } else { Matrix_Translate(pos->x, pos->y, pos->z, MTXMODE_NEW); } Matrix_Scale(scale->x, 1.0f, scale->z, MTXMODE_APPLY); MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_OPA_DISP++, gCircleShadowDL); CLOSE_DISPS(play->state.gfxCtx); } void Actor_RequestQuake(PlayState* play, s16 y, s16 duration) { s16 quakeIndex = Quake_Request(&play->mainCamera, QUAKE_TYPE_3); Quake_SetSpeed(quakeIndex, 20000); Quake_SetPerturbations(quakeIndex, y, 0, 0, 0); Quake_SetDuration(quakeIndex, duration); } void Actor_RequestQuakeWithSpeed(PlayState* play, s16 y, s16 duration, s16 speed) { s16 quakeIndex = Quake_Request(&play->mainCamera, QUAKE_TYPE_3); Quake_SetSpeed(quakeIndex, speed); Quake_SetPerturbations(quakeIndex, y, 0, 0, 0); Quake_SetDuration(quakeIndex, duration); } void Actor_RequestQuakeAndRumble(Actor* actor, PlayState* play, s16 quakeY, s16 quakeDuration) { if (quakeY >= 5) { Rumble_Request(actor->xyzDistToPlayerSq, 255, 20, 150); } else { Rumble_Request(actor->xyzDistToPlayerSq, 180, 20, 100); } Actor_RequestQuake(play, quakeY, quakeDuration); } typedef struct { /* 0x00 */ f32 chainAngle; /* 0x04 */ f32 chainLength; /* 0x08 */ f32 yShift; /* 0x0C */ f32 chainsScale; /* 0x10 */ f32 chainsRotZInit; /* 0x14 */ Gfx* chainDL; /* 0x18 */ Gfx* lockDL; } DoorLockInfo; // size = 0x1C DoorLockInfo sDoorLocksInfo[DOORLOCK_MAX] = { /* DOORLOCK_NORMAL */ { 0.54f, 6000.0f, 5000.0f, 1.0f, 0.0f, gDoorChainDL, gDoorLockDL }, /* DOORLOCK_BOSS */ { 0.644f, 12000.0f, 8000.0f, 1.0f, 0.0f, gBossDoorChainDL, gBossDoorLockDL }, /* DOORLOCK_2 */ { 0.6400000453f, 8500.0f, 8000.0f, 1.75f, 0.1f, gDoorChainDL, gDoorLockDL }, }; /** * Draws chains and lock of a locked door, of the specified `type` (see `DoorLockType`). * `frame` can be 0 to 10, where 0 is "open" and 10 is "closed", the chains slide accordingly. */ void Actor_DrawDoorLock(PlayState* play, s32 frame, s32 type) { s32 pad[2]; MtxF baseMtxF; s32 i; f32 chainsTranslateX; f32 chainsTranslateY; DoorLockInfo* entry = &sDoorLocksInfo[type]; f32 chainRotZ = entry->chainsRotZInit; f32 rotZStep; OPEN_DISPS(play->state.gfxCtx); Matrix_Translate(0.0f, entry->yShift, 500.0f, MTXMODE_APPLY); Matrix_Get(&baseMtxF); chainsTranslateX = sinf(entry->chainAngle - chainRotZ) * -(10 - frame) * 0.1f * entry->chainLength; chainsTranslateY = cosf(entry->chainAngle - chainRotZ) * (10 - frame) * 0.1f * entry->chainLength; for (i = 0; i < 4; i++) { Matrix_Put(&baseMtxF); Matrix_RotateZF(chainRotZ, MTXMODE_APPLY); Matrix_Translate(chainsTranslateX, chainsTranslateY, 0.0f, MTXMODE_APPLY); if (entry->chainsScale != 1.0f) { Matrix_Scale(entry->chainsScale, entry->chainsScale, entry->chainsScale, MTXMODE_APPLY); } MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_OPA_DISP++, entry->chainDL); if ((i % 2) != 0) { rotZStep = 2.0f * entry->chainAngle; } else { rotZStep = M_PIf - (2.0f * entry->chainAngle); } chainRotZ += rotZStep; } Matrix_Put(&baseMtxF); Matrix_Scale(frame * 0.1f, frame * 0.1f, frame * 0.1f, MTXMODE_APPLY); MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_OPA_DISP++, entry->lockDL); CLOSE_DISPS(play->state.gfxCtx); } void Actor_SpawnShieldParticlesMetal(PlayState* play, Vec3f* pos) { CollisionCheck_SpawnShieldParticlesMetal(play, pos); } void Actor_SetColorFilter(Actor* actor, u16 colorFlag, u16 colorIntensityMax, u16 bufFlag, u16 duration) { if ((colorFlag == COLORFILTER_COLORFLAG_GRAY) && !(colorIntensityMax & COLORFILTER_INTENSITY_FLAG)) { Actor_PlaySfx(actor, NA_SE_EN_LIGHT_ARROW_HIT); } actor->colorFilterParams = colorFlag | bufFlag | ((colorIntensityMax & 0xF8) << 5) | duration; actor->colorFilterTimer = duration; } Hilite* func_800BCBF4(Vec3f* arg0, PlayState* play) { Vec3f lightDir; lightDir.x = play->envCtx.dirLight1.params.dir.x; lightDir.y = play->envCtx.dirLight1.params.dir.y; lightDir.z = play->envCtx.dirLight1.params.dir.z; return Hilite_DrawOpa(arg0, &play->view.eye, &lightDir, play->state.gfxCtx); } Hilite* func_800BCC68(Vec3f* arg0, PlayState* play) { Vec3f lightDir; lightDir.x = play->envCtx.dirLight1.params.dir.x; lightDir.y = play->envCtx.dirLight1.params.dir.y; lightDir.z = play->envCtx.dirLight1.params.dir.z; return Hilite_DrawXlu(arg0, &play->view.eye, &lightDir, play->state.gfxCtx); } /** * Calculates the closest position `dstPos` to the input position `srcPos` along the path given by `points`/`numPoints` * Whether the points provided forms a closed-loop path is indicated by `isPathLoop` */ void Actor_GetClosestPosOnPath(Vec3s* points, s32 numPoints, Vec3f* srcPos, Vec3f* dstPos, s32 isPathLoop) { s32 pointIndex; s32 closestPointIndex; s32 useAdjacentLines[2] = { false, // determines whether to use line connecting to previous point in calculations false, // determines whether to use line connecting to next point in calculations }; s32 isRightSideOfAdjacentLines[2] = { false, // determines whether srcPos is on the right side of the line from prev to curr point false, // determines whether srcPos is on the right side of the line from curr to next point }; Vec3f closestPoint; Vec3f closestPos[2]; Vec3f closestPointNext; Vec3f closestPointPrev; f32 distSq; // First used as distSq to closest point, then used as distSq to closest position f32 closestPointDistSq; f32 loopDistSq[2]; s32 i; closestPointIndex = 0; closestPointDistSq = SQ(40000.0f); // Find the point closest to srcPos for (pointIndex = 0; pointIndex < numPoints; pointIndex++) { distSq = Math3D_Dist2DSq(srcPos->x, srcPos->z, points[pointIndex].x, points[pointIndex].z); if (distSq < closestPointDistSq) { closestPointDistSq = distSq; closestPointIndex = pointIndex; } } closestPoint.x = (points + closestPointIndex)->x; closestPoint.z = (points + closestPointIndex)->z; dstPos->y = (points + closestPointIndex)->y; // Analyze point on path immediately previous to the closest point if (closestPointIndex != 0) { // The point previous to the closest point closestPointPrev.x = (points + closestPointIndex - 1)->x; closestPointPrev.z = (points + closestPointIndex - 1)->z; } else if (isPathLoop) { // Closest point is the first point in the path list // Set the previous point to loop around to the the final point on the path closestPointPrev.x = (points + numPoints - 1)->x; closestPointPrev.z = (points + numPoints - 1)->z; } if ((closestPointIndex != 0) || isPathLoop) { // Use the adjacent line useAdjacentLines[0] = Math3D_PointDistSqToLine2DImpl(srcPos->x, srcPos->z, closestPointPrev.x, closestPointPrev.z, closestPoint.x, closestPoint.z, &closestPos[0].x, &closestPos[0].z, &distSq); } // Analyze point on path immediately next to the closest point if (closestPointIndex + 1 != numPoints) { // The point next to the closest point closestPointNext.x = (points + closestPointIndex + 1)->x; closestPointNext.z = (points + closestPointIndex + 1)->z; } else if (isPathLoop) { // Closest point is the final point in the path list // Set the next point to loop around to the the first point on the path closestPointNext.x = (points + 0)->x; closestPointNext.z = (points + 0)->z; } if ((closestPointIndex + 1 != numPoints) || isPathLoop) { useAdjacentLines[1] = Math3D_PointDistSqToLine2DImpl(srcPos->x, srcPos->z, closestPoint.x, closestPoint.z, closestPointNext.x, closestPointNext.z, &closestPos[1].x, &closestPos[1].z, &distSq); } /** * For close-looped paths, they must be defined in a clockwise orientation looking from the top down. * Therefore, `srcPos` being interior of the loop will lead to both lines of `isRightSideOfAdjacentLines` * returning true. */ if (isPathLoop) { isRightSideOfAdjacentLines[0] = ((closestPointPrev.x - srcPos->x) * (closestPoint.z - srcPos->z)) < ((closestPointPrev.z - srcPos->z) * (closestPoint.x - srcPos->x)); isRightSideOfAdjacentLines[1] = ((closestPointNext.z - srcPos->z) * (closestPoint.x - srcPos->x)) < ((closestPoint.z - srcPos->z) * (closestPointNext.x - srcPos->x)); for (i = 0; i < ARRAY_COUNT(loopDistSq); i++) { if (useAdjacentLines[i]) { // Get distSq from srcPos to closestPos loopDistSq[i] = Math3D_Dist2DSq(srcPos->x, srcPos->z, closestPos[i].x, closestPos[i].z); } else { // The closest Pos is not contained within the line-segment loopDistSq[i] = SQ(40000.0f); } } } // Calculate closest position along path if (isPathLoop && ((isRightSideOfAdjacentLines[0] && isRightSideOfAdjacentLines[1]) || (isRightSideOfAdjacentLines[0] && useAdjacentLines[0] && (loopDistSq[0] < loopDistSq[1])) || (isRightSideOfAdjacentLines[1] && useAdjacentLines[1] && (loopDistSq[1] < loopDistSq[0])))) { // srcPos is contained within the closed loop dstPos->x = srcPos->x; dstPos->z = srcPos->z; } else if (useAdjacentLines[0] && useAdjacentLines[1]) { // srcPos is somewhere within the bend of the path if (!isRightSideOfAdjacentLines[0] && !isRightSideOfAdjacentLines[1]) { // srcPos is not inside a loop if (!Math3D_PointDistSqToLine2DImpl(srcPos->x, srcPos->z, closestPos[0].x, closestPos[0].z, closestPos[1].x, closestPos[1].z, &dstPos->x, &dstPos->z, &distSq)) { // The dstPos calculated in Math3D_PointDistSqToLine2DImpl was not valid. // Take the midpoint of the two closest ponits instead dstPos->x = (closestPos[1].x + closestPos[0].x) * 0.5f; dstPos->z = (closestPos[1].z + closestPos[0].z) * 0.5f; } } else if (loopDistSq[1] < loopDistSq[0]) { // Use closest position along the line in the loop connecting the closest point and the next point dstPos->x = closestPos[1].x; dstPos->z = closestPos[1].z; } else { // Use closest position along the ling in the loop connecting the closest point and the prev point dstPos->x = closestPos[0].x; dstPos->z = closestPos[0].z; } } else if (useAdjacentLines[0]) { // Use closest position along line segment connecting the closest point and the prev point dstPos->x = closestPos[0].x; dstPos->z = closestPos[0].z; } else if (useAdjacentLines[1]) { // Use closest position along line segment connecting the closest point and the next point dstPos->x = closestPos[1].x; dstPos->z = closestPos[1].z; } else if (isPathLoop && ((((closestPointPrev.x - srcPos->x) * (closestPointNext.z - srcPos->z)) < ((closestPointPrev.z - srcPos->z) * (closestPointNext.x - srcPos->x))))) { // Inside the line that directly connects the previous point to the next point (inside the bend of a corner) dstPos->x = srcPos->x; dstPos->z = srcPos->z; } else { // The closest point and the closest position are the same (srcPos is near the outer region of a corner) dstPos->x = closestPoint.x; dstPos->z = closestPoint.z; } } /** * Updates NPC talking state. Checks for a talk request and updates * the talkState parameter when a dialog is ongoing. Otherwise checks if * the actor is onscreen, advertises the interaction in a range and sets * the current text id if necessary. * * The talk state values are defined in the NpcTalkState enum. * * @see NpcTalkState * * @param[in,out] talkState Talk state * @param interactRange The interact (talking) range for the actor * @param getTextId Callback for getting the next text id * @param updateTalkState Callback for getting the next talkState value * @return True if a new dialog was started (player talked to the actor). False otherwise. */ s32 Npc_UpdateTalking(PlayState* play, Actor* actor, s16* talkState, f32 interactRange, NpcGetTextIdFunc getTextId, NpcUpdateTalkStateFunc updateTalkState) { if (Actor_TalkOfferAccepted(actor, &play->state)) { *talkState = NPC_TALK_STATE_TALKING; return true; } if (*talkState != NPC_TALK_STATE_IDLE) { *talkState = updateTalkState(play, actor); return false; } if (!Actor_OnScreen(play, actor)) { return false; } if (!Actor_OfferTalk(actor, play, interactRange)) { return false; } actor->textId = getTextId(play, actor); return false; } typedef struct { /* 0x0 */ s16 maxHeadYaw; /* 0x2 */ s16 minHeadPitch; /* 0x4 */ s16 maxHeadPitch; /* 0x6 */ s16 maxTorsoYaw; /* 0x8 */ s16 minTorsoPitch; /* 0xA */ s16 maxTorsoPitch; /* 0xC */ u8 rotateYaw; } NpcTrackingRotLimits; // size = 0x10 typedef struct { /* 0x00 */ NpcTrackingRotLimits rotLimits; // Fields specific to NPC_TRACKING_PLAYER_AUTO_TURN mode /* 0x10 */ f32 autoTurnDistanceRange; // Max distance to player to enable tracking and auto-turn /* 0x14 */ s16 maxYawForPlayerTracking; // Player is tracked if within this yaw } NpcTrackingParams; // size = 0x18 /** * Npc tracking angle limit presets to use with Npc_TrackPoint. * * @see Npc_TrackPoint */ NpcTrackingParams sNpcTrackingPresets[] = { { { 0x1C20, 0xE390, 0x1C70, 0x1554, 0x0000, 0x0000, false }, 170.0f, 0x3FFC }, { { 0x2AA8, 0xEAAC, 0x1554, 0x1554, 0xF8E4, 0x0E38, true }, 170.0f, 0x3FFC }, { { 0x31C4, 0xE390, 0x0E38, 0x0E38, 0xF1C8, 0x071C, true }, 170.0f, 0x3FFC }, { { 0x1554, 0xF1C8, 0x0000, 0x071C, 0xF8E4, 0x0000, true }, 170.0f, 0x3FFC }, { { 0x2AA8, 0xF8E4, 0x071C, 0x0E38, 0xD558, 0x2AA8, true }, 170.0f, 0x3FFC }, { { 0x0000, 0xE390, 0x2AA8, 0x3FFC, 0xF1C8, 0x0E38, true }, 170.0f, 0x3FFC }, { { 0x2AA8, 0xF1C8, 0x0E38, 0x0E38, 0x0000, 0x0000, true }, 0.0f, 0x0000 }, { { 0x2AA8, 0xF1C8, 0x0000, 0x0E38, 0x0000, 0x1C70, true }, 0.0f, 0x0000 }, { { 0x2AA8, 0xF1C8, 0xF1C8, 0x0000, 0x0000, 0x0000, true }, 0.0f, 0x0000 }, { { 0x071C, 0xF1C8, 0x0E38, 0x1C70, 0x0000, 0x0000, true }, 0.0f, 0x0000 }, { { 0x0E38, 0xF1C8, 0x0000, 0x1C70, 0x0000, 0x0E38, true }, 0.0f, 0x0000 }, { { 0x2AA8, 0xE390, 0x1C70, 0x0E38, 0xF1C8, 0x0E38, true }, 0.0f, 0x0000 }, { { 0x18E2, 0xF1C8, 0x0E38, 0x0E38, 0x0000, 0x0000, true }, 0.0f, 0x0000 }, { { 0x2A6C, 0xE390, 0x1C70, 0x1554, 0x0000, 0x0000, false }, 170.0f, 0x3FFC }, }; /** * Smoothly turns the actor's whole body and updates torso and head rotations in * NpcInteractInfo so that the actor tracks the point specified in NpcInteractInfo.trackPos. * Rotations are limited to specified angles. * * Head and torso rotation angles are determined by calculating the pitch and yaw * from the actor position to the given target position. * * The y position of the actor is offset by NpcInteractInfo.yOffset * before calculating the angles. It can be used to configure the height difference * between the actor and the target. * * @param maxHeadYaw maximum head yaw difference from neutral position * @param maxHeadPitch maximum head pitch angle * @param minHeadPitch minimum head pitch angle * @param maxTorsoYaw maximum torso yaw difference from neutral position * @param maxTorsoPitch maximum torso pitch angle * @param minTorsoPitch minimum torso pitch angle * @param rotateYaw if true, the actor's yaw (shape.rot.y) is updated to turn the actor's whole body */ void Npc_TrackPointWithLimits(Actor* actor, NpcInteractInfo* interactInfo, s16 maxHeadYaw, s16 maxHeadPitch, s16 minHeadPitch, s16 maxTorsoYaw, s16 maxTorsoPitch, s16 minTorsoPitch, u8 rotateYaw) { s16 pitchTowardsTarget; s16 yawTowardsTarget; s16 torsoPitch; s16 bodyYawDiff; s16 temp; Vec3f offsetActorPos; offsetActorPos.x = actor->world.pos.x; offsetActorPos.y = actor->world.pos.y + interactInfo->yOffset; offsetActorPos.z = actor->world.pos.z; pitchTowardsTarget = Math_Vec3f_Pitch(&offsetActorPos, &interactInfo->trackPos); yawTowardsTarget = Math_Vec3f_Yaw(&offsetActorPos, &interactInfo->trackPos); bodyYawDiff = Math_Vec3f_Yaw(&actor->world.pos, &interactInfo->trackPos) - actor->shape.rot.y; temp = CLAMP(bodyYawDiff, -maxHeadYaw, maxHeadYaw); Math_SmoothStepToS(&interactInfo->headRot.y, temp, 6, 0x7D0, 1); temp = (ABS_ALT(bodyYawDiff) >= 0x8000) ? 0 : ABS_ALT(bodyYawDiff); interactInfo->headRot.y = CLAMP(interactInfo->headRot.y, -temp, temp); bodyYawDiff -= interactInfo->headRot.y; temp = CLAMP(bodyYawDiff, -maxTorsoYaw, maxTorsoYaw); Math_SmoothStepToS(&interactInfo->torsoRot.y, temp, 6, 0x7D0, 1); temp = (ABS_ALT(bodyYawDiff) >= 0x8000) ? 0 : ABS_ALT(bodyYawDiff); interactInfo->torsoRot.y = CLAMP(interactInfo->torsoRot.y, -temp, temp); if (rotateYaw) { Math_SmoothStepToS(&actor->shape.rot.y, yawTowardsTarget, 6, 0x7D0, 1); } temp = CLAMP(pitchTowardsTarget, minHeadPitch, (s16)(u16)maxHeadPitch); Math_SmoothStepToS(&interactInfo->headRot.x, temp, 6, 0x7D0, 1); torsoPitch = pitchTowardsTarget - interactInfo->headRot.x; temp = CLAMP(torsoPitch, minTorsoPitch, maxTorsoPitch); Math_SmoothStepToS(&interactInfo->torsoRot.x, temp, 6, 0x7D0, 1); } // unused s16 Npc_GetTrackingPresetMaxPlayerYaw(s16 presetIndex) { return sNpcTrackingPresets[presetIndex].maxYawForPlayerTracking; } /** * Handles NPC tracking modes and auto-turning towards the player when * NPC_TRACKING_PLAYER_AUTO_TURN tracking mode is used. * * Returns a tracking mode that will determine which actor limbs * will be rotated towards the target. * * When the player is behind the actor (i.e. not in the yaw range in front of the actor * defined by maxYawForPlayerTracking), the actor will start an auto-turn sequence: * - look forward for 30-60 frames * - turn head to look at the player for 10-20 frames * - look forward for 30-60 frames * - turn the entire body to face the player * * @param distanceRange Max distance to player that tracking and auto-turning will be active for * @param maxYawForPlayerTracking Maximum angle for tracking the player. * @param trackingMode The tracking mode selected by the actor. If this is not * NPC_TRACKING_PLAYER_AUTO_TURN this function does nothing * * @return The tracking mode (NpcTrackingMode) to use for the current frame. */ s16 Npc_UpdateAutoTurn(Actor* actor, NpcInteractInfo* interactInfo, f32 distanceRange, s16 maxYawForPlayerTracking, s16 trackingMode) { s32 pad; s16 yaw; s16 yawDiff; if (trackingMode != NPC_TRACKING_PLAYER_AUTO_TURN) { return trackingMode; } if (interactInfo->talkState != NPC_TALK_STATE_IDLE) { // When talking, always fully turn to face the player return NPC_TRACKING_FULL_BODY; } if (distanceRange < Math_Vec3f_DistXYZ(&actor->world.pos, &interactInfo->trackPos)) { // Player is too far away, do not track interactInfo->autoTurnTimer = 0; interactInfo->autoTurnState = 0; return NPC_TRACKING_NONE; } yaw = Math_Vec3f_Yaw(&actor->world.pos, &interactInfo->trackPos); yawDiff = ABS_ALT(BINANG_SUB(yaw, actor->shape.rot.y)); if (maxYawForPlayerTracking >= yawDiff) { // Player is in front of the actor, track with the head and the torso interactInfo->autoTurnTimer = 0; interactInfo->autoTurnState = 0; return NPC_TRACKING_HEAD_AND_TORSO; } // Player is behind the actor, run the auto-turn sequence. if (DECR(interactInfo->autoTurnTimer) != 0) { // While the timer is still running, return the previous tracking mode return interactInfo->trackingMode; } switch (interactInfo->autoTurnState) { case 0: case 2: // Just stand still, not tracking the player interactInfo->autoTurnTimer = Rand_S16Offset(30, 30); interactInfo->autoTurnState++; return NPC_TRACKING_NONE; case 1: // Glance at the player by only turning the head interactInfo->autoTurnTimer = Rand_S16Offset(10, 10); interactInfo->autoTurnState++; return NPC_TRACKING_HEAD; default: // Auto-turn sequence complete, turn towards the player return NPC_TRACKING_FULL_BODY; } } /** * Rotates the actor's whole body, torso and head tracking the point specified in NpcInteractInfo.trackPos. * Uses angle limits from a preset selected from from sNpcTrackingPresets. * * The trackingMode parameter controls whether the head and torso are turned towards the target. * If not, they are smoothly turned towards zero. Setting the parameter to NPC_TRACKING_FULL_BODY * causes the actor's whole body to be rotated to face the target. * * If NPC_TRACKING_PLAYER_AUTO_TURN is used, the actor will track the player with its head and torso as long * as the player is in front of the actor (within a yaw angle specified in the option preset). * If the player is outside of this angle, the actor will turn to face the player after a while. * * @see Npc_UpdateAutoTurn * @see sNpcTrackingPresets * @see NpcTrackingMode * * @param presetIndex The index to a preset in sNpcTrackingPresets * @param trackingMode A value from NpcTrackingMode enum */ void Npc_TrackPoint(Actor* actor, NpcInteractInfo* interactInfo, s16 presetIndex, s16 trackingMode) { NpcTrackingRotLimits rotLimits; interactInfo->trackingMode = Npc_UpdateAutoTurn(actor, interactInfo, sNpcTrackingPresets[presetIndex].autoTurnDistanceRange, sNpcTrackingPresets[presetIndex].maxYawForPlayerTracking, trackingMode); rotLimits = sNpcTrackingPresets[presetIndex].rotLimits; switch (interactInfo->trackingMode) { case NPC_TRACKING_NONE: rotLimits.maxHeadYaw = 0; rotLimits.maxHeadPitch = 0; rotLimits.minHeadPitch = 0; FALLTHROUGH; case NPC_TRACKING_HEAD: rotLimits.maxTorsoYaw = 0; rotLimits.maxTorsoPitch = 0; rotLimits.minTorsoPitch = 0; FALLTHROUGH; case NPC_TRACKING_HEAD_AND_TORSO: rotLimits.rotateYaw = false; break; default: break; } Npc_TrackPointWithLimits(actor, interactInfo, rotLimits.maxHeadYaw, rotLimits.maxHeadPitch, rotLimits.minHeadPitch, rotLimits.maxTorsoYaw, rotLimits.maxTorsoPitch, rotLimits.minTorsoPitch, rotLimits.rotateYaw); } Gfx D_801AEF88[] = { gsDPSetRenderMode(AA_EN | Z_CMP | Z_UPD | IM_RD | CLR_ON_CVG | CVG_DST_WRAP | ZMODE_XLU | FORCE_BL | G_RM_FOG_SHADE_A, AA_EN | Z_CMP | Z_UPD | IM_RD | CLR_ON_CVG | CVG_DST_WRAP | ZMODE_XLU | FORCE_BL | GBL_c2(G_BL_CLR_IN, G_BL_A_IN, G_BL_CLR_MEM, G_BL_1MA)), gsDPSetAlphaCompare(G_AC_THRESHOLD), gsSPEndDisplayList(), }; Gfx D_801AEFA0[] = { gsSPEndDisplayList(), }; Gfx* func_800BD9A0(GraphicsContext* gfxCtx) { Gfx* gfxHead = GRAPH_ALLOC(gfxCtx, 2 * sizeof(Gfx)); Gfx* gfx = gfxHead; gDPSetRenderMode( gfx++, AA_EN | Z_CMP | Z_UPD | IM_RD | CLR_ON_CVG | CVG_DST_WRAP | ZMODE_XLU | FORCE_BL | G_RM_FOG_SHADE_A, AA_EN | Z_CMP | Z_UPD | IM_RD | CLR_ON_CVG | CVG_DST_WRAP | ZMODE_XLU | FORCE_BL | GBL_c2(G_BL_CLR_IN, G_BL_A_IN, G_BL_CLR_MEM, G_BL_1MA)); gSPEndDisplayList(gfx++); return gfxHead; } // unused void func_800BD9E0(PlayState* play, SkelAnime* skelAnime, OverrideLimbDraw overrideLimbDraw, PostLimbDraw postLimbDraw, Actor* actor, s16 alpha) { OPEN_DISPS(play->state.gfxCtx); Gfx_SetupDL25_Opa(play->state.gfxCtx); gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, alpha); gSPSegment(POLY_OPA_DISP++, 0x0C, gEmptyDL); POLY_OPA_DISP = SkelAnime_DrawFlex(play, skelAnime->skeleton, skelAnime->jointTable, skelAnime->dListCount, overrideLimbDraw, postLimbDraw, actor, POLY_OPA_DISP); CLOSE_DISPS(play->state.gfxCtx); } void func_800BDAA0(PlayState* play, SkelAnime* skelAnime, OverrideLimbDraw overrideLimbDraw, PostLimbDraw postLimbDraw, Actor* actor, s16 alpha) { OPEN_DISPS(play->state.gfxCtx); Gfx_SetupDL25_Xlu(play->state.gfxCtx); gDPSetEnvColor(POLY_XLU_DISP++, 0, 0, 0, alpha); gSPSegment(POLY_XLU_DISP++, 0x0C, func_800BD9A0(play->state.gfxCtx)); POLY_XLU_DISP = SkelAnime_DrawFlex(play, skelAnime->skeleton, skelAnime->jointTable, skelAnime->dListCount, overrideLimbDraw, postLimbDraw, actor, POLY_XLU_DISP); CLOSE_DISPS(play->state.gfxCtx); } // Unused s16 func_800BDB6C(Actor* actor, PlayState* play, s16 arg2, f32 arg3) { Player* player = GET_PLAYER(play); f32 phi_f2; if ((play->csCtx.state != CS_STATE_IDLE) || gDbgCamEnabled) { phi_f2 = Math_Vec3f_DistXYZ(&actor->world.pos, &play->view.eye) * 0.25f; } else { phi_f2 = Math_Vec3f_DistXYZ(&actor->world.pos, &player->actor.world.pos); } if (arg3 < phi_f2) { actor->flags &= ~ACTOR_FLAG_ATTENTION_ENABLED; Math_SmoothStepToS(&arg2, 0, 6, 0x14, 1); } else { actor->flags |= ACTOR_FLAG_ATTENTION_ENABLED; Math_SmoothStepToS(&arg2, 0xFF, 6, 0x14, 1); } return arg2; } void Actor_ChangeAnimationByInfo(SkelAnime* skelAnime, AnimationInfo* animInfo, s32 animIndex) { f32 endFrame; animInfo += animIndex; if (animInfo->frameCount > 0.0f) { endFrame = animInfo->frameCount; } else { endFrame = Animation_GetLastFrame(&animInfo->animation->common); } Animation_Change(skelAnime, animInfo->animation, animInfo->playSpeed, animInfo->startFrame, endFrame, animInfo->mode, animInfo->morphFrames); } /** * Fills two tables with rotation angles that can be used to simulate idle animations. * * The rotation angles are dependent on the current frame, so should be updated regularly, generally every frame. * * This is done for the desired limb by taking either the `sin` of the yTable value or the `cos` of the zTable value, * multiplying by some scale factor (generally 200), and adding that to the already existing rotation. * * Note: With the common scale factor of 200, this effect is practically unnoticeable if the current animation already * has motion involved. * * Note: This function goes unused in favor of `SubS_UpdateFidgetTables`. */ void Actor_UpdateFidgetTables(PlayState* play, s16* fidgetTableY, s16* fidgetTableZ, s32 tableLen) { s32 frames = play->gameplayFrames; s32 i; for (i = 0; i < tableLen; i++) { fidgetTableY[i] = (i * 50 + 0x814) * frames; fidgetTableZ[i] = (i * 50 + 0x940) * frames; } } void Actor_Noop(Actor* actor, PlayState* play) { } #include "z_cheap_proc.c" /** * Finds the first actor instance of a specified Id and category within a given range from * an actor if there is one. If the Id provided is -1, this will look for any actor of the * specified category rather than a specific Id. */ Actor* Actor_FindNearby(PlayState* play, Actor* inActor, s16 actorId, u8 actorCategory, f32 distance) { Actor* actor = play->actorCtx.actorLists[actorCategory].first; while (actor != NULL) { if ((actor == inActor) || ((actorId != -1) && (actorId != actor->id))) { actor = actor->next; continue; } if (Actor_WorldDistXYZToActor(inActor, actor) <= distance) { return actor; } actor = actor->next; } return NULL; } s32 func_800BE184(PlayState* play, Actor* actor, f32 xzDist, s16 arg3, s16 arg4, s16 arg5) { Player* player = GET_PLAYER(play); s16 phi_v0 = BINANG_SUB(BINANG_ROT180(actor->yawTowardsPlayer), player->actor.shape.rot.y); s16 temp_t0 = actor->yawTowardsPlayer - arg5; if ((actor->xzDistToPlayer <= xzDist) && (player->meleeWeaponState != PLAYER_MELEE_WEAPON_STATE_0)) { if ((arg4 >= ABS_ALT(phi_v0)) && (arg3 >= ABS_ALT(temp_t0))) { return true; } } return false; } u8 Actor_ApplyDamage(Actor* actor) { if (actor->colChkInfo.damage >= actor->colChkInfo.health) { actor->colChkInfo.health = 0; } else { actor->colChkInfo.health -= actor->colChkInfo.damage; } return actor->colChkInfo.health; } void Actor_SetDropFlag(Actor* actor, ColliderElement* elem) { ColliderElement* acHitElem = elem->acHitElem; if (acHitElem == NULL) { actor->dropFlag = DROPFLAG_NONE; } else if (acHitElem->atDmgInfo.dmgFlags & DMG_FIRE_ARROW) { actor->dropFlag = DROPFLAG_1; } else if (acHitElem->atDmgInfo.dmgFlags & DMG_ICE_ARROW) { actor->dropFlag = DROPFLAG_2; } else if (acHitElem->atDmgInfo.dmgFlags & DMG_LIGHT_ARROW) { actor->dropFlag = DROPFLAG_20; } else { actor->dropFlag = DROPFLAG_NONE; } } void Actor_SetDropFlagJntSph(Actor* actor, ColliderJntSph* jntSph) { s32 i; ColliderJntSphElement* jntSphElem; ColliderElement* acHitElem; s32 flag; actor->dropFlag = DROPFLAG_NONE; for (i = jntSph->count - 1; i >= 0; i--) { jntSphElem = &jntSph->elements[i]; acHitElem = jntSphElem->base.acHitElem; if (acHitElem == NULL) { flag = DROPFLAG_NONE; } else { s32 dmgFlags = acHitElem->atDmgInfo.dmgFlags; if (dmgFlags & DMG_FIRE_ARROW) { flag = DROPFLAG_1; } else if (dmgFlags & DMG_ICE_ARROW) { flag = DROPFLAG_2; } else { flag = (dmgFlags & DMG_LIGHT_ARROW) ? DROPFLAG_20 : DROPFLAG_NONE; } } actor->dropFlag |= flag; } } void func_800BE33C(Vec3f* arg0, Vec3f* arg1, Vec3s* dst, s32 arg3) { f32 xDiff = arg1->x - arg0->x; f32 zDiff = arg1->z - arg0->z; f32 yDiff = arg3 ? (arg1->y - arg0->y) : (arg0->y - arg1->y); dst->y = Math_Atan2S_XY(zDiff, xDiff); dst->x = Math_Atan2S_XY(sqrtf(SQ(xDiff) + SQ(zDiff)), yDiff); } void func_800BE3D0(Actor* actor, s16 angle, Vec3s* arg2) { f32 floorPolyNormalX; f32 floorPolyNormalY; f32 floorPolyNormalZ; f32 sp38; f32 sp34; f32 sp30; f32 sp2C; s32 pad[3]; if (actor->floorPoly != NULL) { CollisionPoly* floorPoly = actor->floorPoly; floorPolyNormalX = COLPOLY_GET_NORMAL(floorPoly->normal.x); floorPolyNormalY = COLPOLY_GET_NORMAL(floorPoly->normal.y); floorPolyNormalZ = COLPOLY_GET_NORMAL(floorPoly->normal.z); sp38 = Math_SinS(angle); sp34 = Math_CosS(angle); arg2->x = (s16)-Math_Atan2S((-(floorPolyNormalX * sp38) - (floorPolyNormalZ * sp34)) * floorPolyNormalY, 1.0f); sp2C = Math_SinS(angle - 0x3FF7); sp30 = Math_CosS(angle - 0x3FF7); arg2->z = (s16)-Math_Atan2S((-(floorPolyNormalX * sp2C) - (floorPolyNormalZ * sp30)) * floorPolyNormalY, 1.0f); } } void func_800BE504(Actor* actor, ColliderCylinder* cyl) { // Checks if was hit by either DMG_NORMAL_ARROW, DMG_FIRE_ARROW, DMG_ICE_ARROW, DMG_LIGHT_ARROW or DMG_DEKU_BUBBLE if ((cyl->elem.acHitElem->atDmgInfo.dmgFlags & (0x10000 | 0x2000 | 0x1000 | 0x800 | 0x20))) { actor->world.rot.y = cyl->base.ac->shape.rot.y; } else { actor->world.rot.y = Actor_WorldYawTowardActor(cyl->base.ac, actor); } } void func_800BE568(Actor* actor, ColliderSphere* sph) { if (sph->elem.acHitElem->atDmgInfo.dmgFlags & (0x10000 | 0x2000 | 0x1000 | 0x800 | 0x20)) { actor->world.rot.y = sph->base.ac->shape.rot.y; } else { actor->world.rot.y = Actor_WorldYawTowardActor(sph->base.ac, actor); } } void func_800BE5CC(Actor* actor, ColliderJntSph* jntSph, s32 elemIndex) { if (jntSph->elements[elemIndex].base.acHitElem->atDmgInfo.dmgFlags & (0x10000 | 0x2000 | 0x1000 | 0x800 | 0x20)) { actor->world.rot.y = jntSph->base.ac->shape.rot.y; } else { actor->world.rot.y = Actor_WorldYawTowardActor(jntSph->base.ac, actor); } } s32 Actor_IsSmallChest(struct EnBox* chest) { if (chest->type == ENBOX_TYPE_SMALL || chest->type == ENBOX_TYPE_SMALL_INVISIBLE || chest->type == ENBOX_TYPE_SMALL_ROOM_CLEAR || chest->type == ENBOX_TYPE_SMALL_SWITCH_FLAG_FALL || chest->type == ENBOX_TYPE_SMALL_SWITCH_FLAG) { return true; } return false; } TexturePtr sElectricSparkTextures[] = { gElectricSpark1Tex, gElectricSpark2Tex, gElectricSpark3Tex, gElectricSpark4Tex, }; /** * Draw common damage effects applied to each body part provided in bodyPartsPos */ void Actor_DrawDamageEffects(PlayState* play, Actor* actor, Vec3f bodyPartsPos[], s16 bodyPartsCount, f32 effectScale, f32 frozenSteamScale, f32 effectAlpha, u8 type) { if (effectAlpha > 0.001f) { s32 twoTexScrollParam; s16 bodyPartIndex; MtxF* currentMatrix; f32 alpha; f32 frozenScale; f32 lightOrbsScale; f32 electricSparksScale; f32 steamScale; Vec3f* bodyPartsPosStart = bodyPartsPos; u32 gameplayFrames = play->gameplayFrames; f32 effectAlphaScaled; currentMatrix = Matrix_GetCurrent(); // Apply sfx along with damage effect if ((actor != NULL) && (effectAlpha > 0.05f) && (play->gameOverCtx.state == GAMEOVER_INACTIVE)) { if (type == ACTOR_DRAW_DMGEFF_FIRE) { Actor_PlaySfx(actor, NA_SE_EV_BURN_OUT - SFX_FLAG); } else if (type == ACTOR_DRAW_DMGEFF_BLUE_FIRE) { Actor_PlaySfx(actor, NA_SE_EN_COMMON_EXTINCT_LEV - SFX_FLAG); } else if (type == ACTOR_DRAW_DMGEFF_FROZEN_SFX) { Actor_PlaySfx(actor, NA_SE_EV_ICE_FREEZE - SFX_FLAG); } else if ((type == ACTOR_DRAW_DMGEFF_LIGHT_ORBS) || (type == ACTOR_DRAW_DMGEFF_BLUE_LIGHT_ORBS)) { Actor_PlaySfx(actor, NA_SE_EN_COMMON_DEADLIGHT - SFX_FLAG); } } OPEN_DISPS(play->state.gfxCtx); Gfx_SetupDL25_Xlu(play->state.gfxCtx); switch (type) { case ACTOR_DRAW_DMGEFF_FROZEN_NO_SFX: case ACTOR_DRAW_DMGEFF_FROZEN_SFX: frozenScale = ((KREG(19) * 0.01f) + 2.3f) * effectScale; steamScale = ((KREG(28) * 0.0001f) + 0.035f) * frozenSteamScale; func_800BCC68(bodyPartsPos, play); // Setup to draw ice over frozen actor gSPSegment(POLY_XLU_DISP++, 0x08, Gfx_TwoTexScroll(play->state.gfxCtx, 0, 0, gameplayFrames & 0xFF, 32, 16, 1, 0, (gameplayFrames * 2) & 0xFF, 64, 32)); gDPSetPrimColor(POLY_XLU_DISP++, 0, 0x80, 170, 255, 255, 255); gSPDisplayList(POLY_XLU_DISP++, gEffIceFragment2MaterialDL); effectAlphaScaled = effectAlpha * 255.0f; // Apply and draw ice over each body part of frozen actor for (bodyPartIndex = 0; bodyPartIndex < bodyPartsCount; bodyPartIndex++, bodyPartsPos++) { alpha = bodyPartIndex & 3; alpha = effectAlphaScaled - (30.0f * alpha); if (effectAlphaScaled < (30.0f * (bodyPartIndex & 3))) { alpha = 0.0f; } if (alpha > 255.0f) { alpha = 255.0f; } gDPSetEnvColor(POLY_XLU_DISP++, KREG(20) + 200, KREG(21) + 200, KREG(22) + 255, (u8)alpha); Matrix_Translate(bodyPartsPos->x, bodyPartsPos->y, bodyPartsPos->z, MTXMODE_NEW); Matrix_Scale(frozenScale, frozenScale, frozenScale, MTXMODE_APPLY); if (bodyPartIndex & 1) { Matrix_RotateYF(M_PIf, MTXMODE_APPLY); } if (bodyPartIndex & 2) { Matrix_RotateZF(M_PIf, MTXMODE_APPLY); } MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_XLU_DISP++, gEffIceFragment2ModelDL); } bodyPartsPos = bodyPartsPosStart; // reset bodyPartsPos // Setup to draw steam over frozen actor gDPSetColorDither(POLY_XLU_DISP++, G_CD_BAYER); gDPSetAlphaDither(POLY_XLU_DISP++, G_AD_PATTERN); gSPDisplayList(POLY_XLU_DISP++, gFrozenSteamMaterialDL); alpha = effectAlpha * 100.0f; if (alpha > 100.0f) { alpha = 100.0f; } gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, 195, 225, 235, (u8)alpha); // Apply and draw steam over each body part of frozen actor for (bodyPartIndex = 0; bodyPartIndex < bodyPartsCount; bodyPartIndex++, bodyPartsPos++) { twoTexScrollParam = ((bodyPartIndex * 3) + gameplayFrames); gSPSegment(POLY_XLU_DISP++, 0x08, Gfx_TwoTexScroll(play->state.gfxCtx, 0, twoTexScrollParam * 3, twoTexScrollParam * -12, 32, 64, 1, 0, 0, 32, 32)); Matrix_Translate(bodyPartsPos->x, bodyPartsPos->y, bodyPartsPos->z, MTXMODE_NEW); Matrix_ReplaceRotation(&play->billboardMtxF); Matrix_Scale(steamScale, steamScale, 1.0f, MTXMODE_APPLY); MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_XLU_DISP++, gFrozenSteamModelDL); } break; case ACTOR_DRAW_DMGEFF_FIRE: case ACTOR_DRAW_DMGEFF_BLUE_FIRE: if (type == ACTOR_DRAW_DMGEFF_FIRE) { gDPSetEnvColor(POLY_XLU_DISP++, 255, 10, 0, 0); } else { gDPSetEnvColor(POLY_XLU_DISP++, 0, 255, 255, 0); // Reuse type for blue primitive color type = 255; } Matrix_Put(&play->billboardMtxF); Matrix_Scale((effectScale * 0.005f) * 1.35f, (effectScale * 0.005f), (effectScale * 0.005f) * 1.35f, MTXMODE_APPLY); effectAlphaScaled = effectAlpha * 255.0f; // Apply and draw fire on every body part for (bodyPartIndex = 0; bodyPartIndex < bodyPartsCount; bodyPartIndex++, bodyPartsPos++) { alpha = bodyPartIndex & 3; alpha = effectAlphaScaled - 30.0f * alpha; if (effectAlphaScaled < (30.0f * (bodyPartIndex & 3))) { alpha = 0.0f; } if (alpha > 255.0f) { alpha = 255.0f; } // Use type for blue primitive color // = 0 for ACTOR_DRAW_DMGEFF_FIRE // = 255 for ACTOR_DRAW_DMGEFF_BLUE_FIRE gDPSetPrimColor(POLY_XLU_DISP++, 0x80, 0x80, 255, 255, type, (u8)alpha); gSPSegment(POLY_XLU_DISP++, 0x08, Gfx_TwoTexScroll(play->state.gfxCtx, 0, 0, 0, 32, 64, 1, 0, ((bodyPartIndex * 10 + gameplayFrames) * -20) & 0x1FF, 32, 128)); Matrix_RotateYF(M_PIf, MTXMODE_APPLY); currentMatrix->mf[3][0] = bodyPartsPos->x; currentMatrix->mf[3][1] = bodyPartsPos->y; currentMatrix->mf[3][2] = bodyPartsPos->z; MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_XLU_DISP++, gEffFire1DL); } break; case ACTOR_DRAW_DMGEFF_LIGHT_ORBS: case ACTOR_DRAW_DMGEFF_BLUE_LIGHT_ORBS: // Setup to draw light orbs on actor lightOrbsScale = ((KREG(19) * 0.01f) + 4.0f) * effectScale; gSPDisplayList(POLY_XLU_DISP++, gLightOrbMaterial1DL); alpha = effectAlpha * 255.0f; if (alpha > 255.0f) { alpha = 255.0f; } if (type == ACTOR_DRAW_DMGEFF_BLUE_LIGHT_ORBS) { gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, (u8)(sREG(16) + 255), (u8)(sREG(17) + 255), (u8)(sREG(18) + 255), (u8)alpha); gDPSetEnvColor(POLY_XLU_DISP++, (u8)sREG(19), (u8)(sREG(20) + 255), (u8)(sREG(21) + 255), 128); } else { gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, 255, 255, 200, (u8)alpha); gDPSetEnvColor(POLY_XLU_DISP++, 255, 255, 100, 128); } Matrix_Put(&play->billboardMtxF); Matrix_Scale(lightOrbsScale, lightOrbsScale, 1.0f, MTXMODE_APPLY); // Apply and draw a light orb over each body part of frozen actor for (bodyPartIndex = 0; bodyPartIndex < bodyPartsCount; bodyPartIndex++, bodyPartsPos++) { Matrix_RotateZF(Rand_CenteredFloat(2 * M_PIf), MTXMODE_APPLY); currentMatrix->mf[3][0] = bodyPartsPos->x; currentMatrix->mf[3][1] = bodyPartsPos->y; currentMatrix->mf[3][2] = bodyPartsPos->z; MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_XLU_DISP++, gLightOrbModelDL); } break; case ACTOR_DRAW_DMGEFF_ELECTRIC_SPARKS_SMALL: case ACTOR_DRAW_DMGEFF_ELECTRIC_SPARKS_MEDIUM: case ACTOR_DRAW_DMGEFF_ELECTRIC_SPARKS_LARGE: if (type == ACTOR_DRAW_DMGEFF_ELECTRIC_SPARKS_SMALL) { electricSparksScale = (KREG(19) * 0.01f + 1.0f) * effectScale; } else if (type == ACTOR_DRAW_DMGEFF_ELECTRIC_SPARKS_MEDIUM) { electricSparksScale = (KREG(19) * 0.01f + 1.5f) * effectScale; } else { electricSparksScale = (KREG(19) * 0.01f + 2.0f) * effectScale; } gSPSegment(POLY_XLU_DISP++, 0x08, Lib_SegmentedToVirtual(sElectricSparkTextures[play->gameplayFrames % 4])); gSPDisplayList(POLY_XLU_DISP++, gElectricSparkMaterialDL); gDPSetPrimColor(POLY_XLU_DISP++, 0, 0, (u8)(sREG(16) + 255), (u8)(sREG(17) + 255), (u8)(sREG(18) + 150), (u8)(sREG(19) + 255)); gDPSetEnvColor(POLY_XLU_DISP++, (u8)(sREG(20) + 255), (u8)(sREG(21) + 255), (u8)sREG(22), (u8)sREG(23)); Matrix_Put(&play->billboardMtxF); Matrix_Scale(electricSparksScale, electricSparksScale, electricSparksScale, MTXMODE_APPLY); // Every body part draws two electric sparks at random orientations for (bodyPartIndex = 0; bodyPartIndex < bodyPartsCount; bodyPartIndex++, bodyPartsPos++) { // first electric spark Matrix_RotateXFApply(Rand_ZeroFloat(2 * M_PIf)); Matrix_RotateZF(Rand_ZeroFloat(2 * M_PIf), MTXMODE_APPLY); currentMatrix->mf[3][0] = Rand_CenteredFloat((f32)sREG(24) + 30.0f) + bodyPartsPos->x; currentMatrix->mf[3][1] = Rand_CenteredFloat((f32)sREG(24) + 30.0f) + bodyPartsPos->y; currentMatrix->mf[3][2] = Rand_CenteredFloat((f32)sREG(24) + 30.0f) + bodyPartsPos->z; MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_XLU_DISP++, gElectricSparkModelDL); // second electric spark Matrix_RotateXFApply(Rand_ZeroFloat(2 * M_PIf)); Matrix_RotateZF(Rand_ZeroFloat(2 * M_PIf), MTXMODE_APPLY); currentMatrix->mf[3][0] = Rand_CenteredFloat((f32)sREG(24) + 30.0f) + bodyPartsPos->x; currentMatrix->mf[3][1] = Rand_CenteredFloat((f32)sREG(24) + 30.0f) + bodyPartsPos->y; currentMatrix->mf[3][2] = Rand_CenteredFloat((f32)sREG(24) + 30.0f) + bodyPartsPos->z; MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, play->state.gfxCtx); gSPDisplayList(POLY_XLU_DISP++, gElectricSparkModelDL); } break; } CLOSE_DISPS(play->state.gfxCtx); } } void Actor_SpawnIceEffects(PlayState* play, Actor* actor, Vec3f bodyPartsPos[], s32 bodyPartsCount, s32 effectsPerBodyPart, f32 scale, f32 scaleRange) { static Color_RGBA8 sPrimColor = { 170, 255, 255, 255 }; static Color_RGBA8 sEnvColor = { 200, 200, 255, 255 }; static Vec3f sAccel = { 0.0f, -1.0f, 0.0f }; s32 i; s32 pad; Vec3f velocity; s16 randomYaw; s16 yaw; s32 j; SoundSource_PlaySfxAtFixedWorldPos(play, &actor->world.pos, 30, NA_SE_EV_ICE_BROKEN); for (i = 0; i < bodyPartsCount; i++) { yaw = Actor_WorldYawTowardPoint(actor, bodyPartsPos); for (j = 0; j < effectsPerBodyPart; j++) { randomYaw = ((s32)Rand_Next() >> 0x13) + yaw; velocity.z = Rand_ZeroFloat(5.0f); velocity.x = Math_SinS(randomYaw) * velocity.z; velocity.y = Rand_ZeroFloat(4.0f) + 8.0f; velocity.z *= Math_CosS(randomYaw); EffectSsEnIce_Spawn(play, bodyPartsPos, Rand_ZeroFloat(scaleRange) + scale, &velocity, &sAccel, &sPrimColor, &sEnvColor, 30); } bodyPartsPos++; } }