Shipwright/soh/src/overlays/actors/ovl_En_St/z_en_st.c

1090 lines
34 KiB
C

/*
* File: z_en_st.c
* Overlay: ovl_En_St
* Description: Skulltula (normal, big, invisible)
*/
#include "z_en_st.h"
#include "objects/object_st/object_st.h"
#define FLAGS (ACTOR_FLAG_0 | ACTOR_FLAG_2 | ACTOR_FLAG_4 | ACTOR_FLAG_5)
void EnSt_Init(Actor* thisx, GlobalContext* globalCtx);
void EnSt_Destroy(Actor* thisx, GlobalContext* globalCtx);
void EnSt_Update(Actor* thisx, GlobalContext* globalCtx);
void EnSt_Draw(Actor* thisx, GlobalContext* globalCtx);
void EnSt_ReturnToCeiling(EnSt* this, GlobalContext* globalCtx);
void EnSt_MoveToGround(EnSt* this, GlobalContext* globalCtx);
void EnSt_StartOnCeilingOrGround(EnSt* this, GlobalContext* globalCtx);
void EnSt_WaitOnGround(EnSt* this, GlobalContext* globalCtx);
void EnSt_Die(EnSt* this, GlobalContext* globalCtx);
void EnSt_BounceAround(EnSt* this, GlobalContext* globalCtx);
void EnSt_FinishBouncing(EnSt* this, GlobalContext* globalCtx);
#include "overlays/ovl_En_St/ovl_En_St.h"
const ActorInit En_St_InitVars = {
ACTOR_EN_ST,
ACTORCAT_ENEMY,
FLAGS,
OBJECT_ST,
sizeof(EnSt),
(ActorFunc)EnSt_Init,
(ActorFunc)EnSt_Destroy,
(ActorFunc)EnSt_Update,
(ActorFunc)EnSt_Draw,
NULL,
};
static ColliderCylinderInit sCylinderInit = {
{
COLTYPE_HIT6,
AT_NONE,
AC_ON | AC_TYPE_PLAYER,
OC1_NONE,
OC2_TYPE_1,
COLSHAPE_CYLINDER,
},
{
ELEMTYPE_UNK0,
{ 0x00000000, 0x00, 0x00 },
{ 0x00000000, 0x00, 0x00 },
TOUCH_ON | TOUCH_SFX_NORMAL,
BUMP_ON,
OCELEM_NONE,
},
{ 32, 50, -24, { 0, 0, 0 } },
};
static CollisionCheckInfoInit2 sColChkInit = { 2, 0, 0, 0, MASS_IMMOVABLE };
static ColliderCylinderInit sCylinderInit2 = {
{
COLTYPE_HIT6,
AT_NONE,
AC_NONE,
OC1_ON | OC1_TYPE_ALL,
OC2_TYPE_1,
COLSHAPE_CYLINDER,
},
{
ELEMTYPE_UNK0,
{ 0x00000000, 0x00, 0x00 },
{ 0x00000000, 0x00, 0x00 },
TOUCH_NONE,
BUMP_NONE,
OCELEM_ON,
},
{ 20, 60, -30, { 0, 0, 0 } },
};
static ColliderJntSphElementInit sJntSphElementsInit[1] = {
{
{
ELEMTYPE_UNK0,
{ 0xFFCFFFFF, 0x00, 0x04 },
{ 0x00000000, 0x00, 0x00 },
TOUCH_ON | TOUCH_SFX_NORMAL,
BUMP_NONE,
OCELEM_ON,
},
{ 1, { { 0, -240, 0 }, 28 }, 100 },
},
};
static ColliderJntSphInit sJntSphInit = {
{
COLTYPE_HIT6,
AT_ON | AT_TYPE_ENEMY,
AC_NONE,
OC1_ON | OC1_TYPE_ALL,
OC2_TYPE_1,
COLSHAPE_JNTSPH,
},
1,
sJntSphElementsInit,
};
typedef enum {
/* 0 */ ENST_ANIM_0,
/* 1 */ ENST_ANIM_1,
/* 2 */ ENST_ANIM_2,
/* 3 */ ENST_ANIM_3,
/* 4 */ ENST_ANIM_4,
/* 5 */ ENST_ANIM_5,
/* 6 */ ENST_ANIM_6,
/* 7 */ ENST_ANIM_7
} EnStAnimation;
static AnimationInfo sAnimationInfo[] = {
{ &object_st_Anim_000304, 1.0f, 0.0f, -1.0f, ANIMMODE_LOOP_INTERP, 0.0f },
{ &object_st_Anim_005B98, 1.0f, 0.0f, -1.0f, ANIMMODE_ONCE_INTERP, -8.0f },
{ &object_st_Anim_000304, 4.0f, 0.0f, -1.0f, ANIMMODE_ONCE_INTERP, -8.0f },
{ &object_st_Anim_000304, 1.0f, 0.0f, -1.0f, ANIMMODE_LOOP_INTERP, -8.0f },
{ &object_st_Anim_0055A8, 1.0f, 0.0f, -1.0f, ANIMMODE_ONCE_INTERP, -8.0f },
{ &object_st_Anim_000304, 8.0f, 0.0f, -1.0f, ANIMMODE_LOOP_INTERP, -8.0f },
{ &object_st_Anim_000304, 6.0f, 0.0f, -1.0f, ANIMMODE_LOOP_INTERP, -8.0f },
{ &object_st_Anim_005B98, 2.0f, 0.0f, -1.0f, ANIMMODE_LOOP_INTERP, -8.0f },
};
void EnSt_SetupAction(EnSt* this, EnStActionFunc actionFunc) {
this->actionFunc = actionFunc;
}
/**
* Spawns `dustCnt` dust particles in a random pattern around the Skulltula
*/
void EnSt_SpawnDust(EnSt* this, GlobalContext* globalCtx, s32 dustCnt) {
Color_RGBA8 primColor = { 170, 130, 90, 255 };
Color_RGBA8 envColor = { 100, 60, 20, 0 };
Vec3f dustVel = { 0.0f, 0.0f, 0.0f };
Vec3f dustAccel = { 0.0f, 0.3f, 0.0f };
Vec3f dustPos;
s16 yAngle;
s32 i;
yAngle = (Rand_ZeroOne() - 0.5f) * 65536.0f;
dustPos.y = this->actor.floorHeight;
for (i = dustCnt; i >= 0; i--, yAngle += (s16)(0x10000 / dustCnt)) {
dustAccel.x = (Rand_ZeroOne() - 0.5f) * 4.0f;
dustAccel.z = (Rand_ZeroOne() - 0.5f) * 4.0f;
dustPos.x = this->actor.world.pos.x + (Math_SinS(yAngle) * 22.0f);
dustPos.z = this->actor.world.pos.z + (Math_CosS(yAngle) * 22.0f);
func_8002836C(globalCtx, &dustPos, &dustVel, &dustAccel, &primColor, &envColor, 120, 40, 10);
}
}
void EnSt_SpawnBlastEffect(EnSt* this, GlobalContext* globalCtx) {
Vec3f zeroVec = { 0.0f, 0.0f, 0.0f };
Vec3f blastPos;
blastPos.x = this->actor.world.pos.x;
blastPos.y = this->actor.floorHeight;
blastPos.z = this->actor.world.pos.z;
EffectSsBlast_SpawnWhiteCustomScale(globalCtx, &blastPos, &zeroVec, &zeroVec, 100, 220, 8);
}
void EnSt_SpawnDeadEffect(EnSt* this, GlobalContext* globalCtx) {
Vec3f zeroVec = { 0.0f, 0.0f, 0.0f };
Vec3f firePos;
firePos.x = this->actor.world.pos.x + ((Rand_ZeroOne() - 0.5f) * 60.0f);
firePos.y = (this->actor.world.pos.y + 10.0f) + ((Rand_ZeroOne() - 0.5f) * 45.0f);
firePos.z = this->actor.world.pos.z + ((Rand_ZeroOne() - 0.5f) * 60.0f);
EffectSsDeadDb_Spawn(globalCtx, &firePos, &zeroVec, &zeroVec, 100, 0, 255, 255, 255, 255, 255, 0, 0, 1, 9, true);
}
s32 EnSt_CreateBlureEffect(GlobalContext* globalCtx) {
EffectBlureInit1 blureInit;
u8 p1StartColor[] = { 255, 255, 255, 75 };
u8 p2StartColor[] = { 255, 255, 255, 75 };
u8 p1EndColor[] = { 255, 255, 255, 0 };
u8 p2EndColor[] = { 255, 255, 255, 0 };
s32 i;
s32 blureIdx;
for (i = 0; i < 4; i++) {
blureInit.p1StartColor[i] = p1StartColor[i];
blureInit.p2StartColor[i] = p2StartColor[i];
blureInit.p1EndColor[i] = p1EndColor[i];
blureInit.p2EndColor[i] = p2EndColor[i];
}
blureInit.elemDuration = 6;
blureInit.unkFlag = 0;
blureInit.calcMode = 3;
Effect_Add(globalCtx, &blureIdx, EFFECT_BLURE1, 0, 0, &blureInit);
return blureIdx;
}
/**
* Checks for the position of the ceiling above the Skulltula.
* If no ceiling is found it is set to 1000 units above the Skulltula
*/
s32 EnSt_CheckCeilingPos(EnSt* this, GlobalContext* globalCtx) {
CollisionPoly* poly;
s32 bgId;
Vec3f checkPos;
checkPos.x = this->actor.world.pos.x;
checkPos.y = this->actor.world.pos.y + 1000.0f;
checkPos.z = this->actor.world.pos.z;
if (!BgCheck_EntityLineTest1(&globalCtx->colCtx, &this->actor.world.pos, &checkPos, &this->ceilingPos, &poly, false,
false, true, true, &bgId)) {
return false;
}
this->unusedPos = this->actor.world.pos;
this->unusedPos.y -= 100.0f;
return true;
}
void EnSt_AddBlurVertex(EnSt* this) {
Vec3f v1 = { 834.0f, 834.0f, 0.0f };
Vec3f v2 = { 834.0f, -584.0f, 0.0f };
Vec3f v1Pos;
Vec3f v2Pos;
v1.x *= this->colliderScale;
v1.y *= this->colliderScale;
v1.z *= this->colliderScale;
v2.x *= this->colliderScale;
v2.y *= this->colliderScale;
v2.z *= this->colliderScale;
Matrix_Push();
Matrix_MultVec3f(&v1, &v1Pos);
Matrix_MultVec3f(&v2, &v2Pos);
Matrix_Pop();
EffectBlure_AddVertex(Effect_GetByIndex(this->blureIdx), &v1Pos, &v2Pos);
}
void EnSt_AddBlurSpace(EnSt* this) {
EffectBlure_AddSpace(Effect_GetByIndex(this->blureIdx));
}
void EnSt_SetWaitingAnimation(EnSt* this) {
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_3);
}
void EnSt_SetReturnToCeilingAnimation(EnSt* this) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_UP);
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_2);
}
void EnSt_SetLandAnimation(EnSt* this) {
this->actor.world.pos.y = this->actor.floorHeight + this->floorHeightOffset;
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_4);
this->sfxTimer = 0;
this->animFrames = this->skelAnime.animLength;
}
void EnSt_SetDropAnimAndVel(EnSt* this) {
if (this->takeDamageSpinTimer == 0) {
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_4);
this->animFrames = this->skelAnime.animLength;
}
this->sfxTimer = 0;
this->actor.velocity.y = -10.0f;
}
/**
* Initalizes the Skulltula's 6 cylinders, and sphere collider.
*/
void EnSt_InitColliders(EnSt* this, GlobalContext* globalCtx) {
ColliderCylinderInit* cylinders[6] = {
&sCylinderInit, &sCylinderInit, &sCylinderInit, &sCylinderInit2, &sCylinderInit2, &sCylinderInit2,
};
s32 i;
s32 pad;
for (i = 0; i < ARRAY_COUNT(cylinders); i++) {
Collider_InitCylinder(globalCtx, &this->colCylinder[i]);
Collider_SetCylinder(globalCtx, &this->colCylinder[i], &this->actor, cylinders[i]);
}
this->colCylinder[0].info.bumper.dmgFlags = 0x0003F8F9;
this->colCylinder[1].info.bumper.dmgFlags = 0xFFC00706;
this->colCylinder[2].base.colType = COLTYPE_METAL;
this->colCylinder[2].info.bumperFlags = BUMP_ON | BUMP_HOOKABLE | BUMP_NO_AT_INFO;
this->colCylinder[2].info.elemType = ELEMTYPE_UNK2;
this->colCylinder[2].info.bumper.dmgFlags = 0xFFCC0706;
CollisionCheck_SetInfo2(&this->actor.colChkInfo, DamageTable_Get(2), &sColChkInit);
Collider_InitJntSph(globalCtx, &this->colSph);
Collider_SetJntSph(globalCtx, &this->colSph, &this->actor, &sJntSphInit, this->colSphItems);
}
void EnSt_CheckBodyStickHit(EnSt* this, GlobalContext* globalCtx) {
ColliderInfo* body = &this->colCylinder[0].info;
Player* player = GET_PLAYER(globalCtx);
if (player->unk_860 != 0) {
body->bumper.dmgFlags |= 2;
this->colCylinder[1].info.bumper.dmgFlags &= ~2;
this->colCylinder[2].info.bumper.dmgFlags &= ~2;
} else {
body->bumper.dmgFlags &= ~2;
this->colCylinder[1].info.bumper.dmgFlags |= 2;
this->colCylinder[2].info.bumper.dmgFlags |= 2;
}
}
void EnSt_SetBodyCylinderAC(EnSt* this, GlobalContext* globalCtx) {
Collider_UpdateCylinder(&this->actor, &this->colCylinder[0]);
CollisionCheck_SetAC(globalCtx, &globalCtx->colChkCtx, &this->colCylinder[0].base);
}
void EnSt_SetLegsCylinderAC(EnSt* this, GlobalContext* globalCtx) {
s16 angleTowardsLink = ABS((s16)(this->actor.yawTowardsPlayer - this->actor.shape.rot.y));
if (angleTowardsLink < 0x3FFC) {
Collider_UpdateCylinder(&this->actor, &this->colCylinder[2]);
CollisionCheck_SetAC(globalCtx, &globalCtx->colChkCtx, &this->colCylinder[2].base);
} else {
Collider_UpdateCylinder(&this->actor, &this->colCylinder[1]);
CollisionCheck_SetAC(globalCtx, &globalCtx->colChkCtx, &this->colCylinder[1].base);
}
}
s32 EnSt_SetCylinderOC(EnSt* this, GlobalContext* globalCtx) {
Vec3f cyloffsets[] = {
{ 40.0f, 0.0f, 0.0f },
{ 0.0f, 0.0f, 0.0f },
{ -40.0f, 0.0f, 0.0f },
};
Vec3f cylPos;
s32 i;
for (i = 0; i < 3; i++) {
cylPos = this->actor.world.pos;
cyloffsets[i].x *= this->colliderScale;
cyloffsets[i].y *= this->colliderScale;
cyloffsets[i].z *= this->colliderScale;
Matrix_Push();
Matrix_Translate(cylPos.x, cylPos.y, cylPos.z, MTXMODE_NEW);
Matrix_RotateY((this->initalYaw / 32768.0f) * M_PI, MTXMODE_APPLY);
Matrix_MultVec3f(&cyloffsets[i], &cylPos);
Matrix_Pop();
this->colCylinder[i + 3].dim.pos.x = cylPos.x;
this->colCylinder[i + 3].dim.pos.y = cylPos.y;
this->colCylinder[i + 3].dim.pos.z = cylPos.z;
CollisionCheck_SetOC(globalCtx, &globalCtx->colChkCtx, &this->colCylinder[i + 3].base);
}
return true;
}
void EnSt_UpdateCylinders(EnSt* this, GlobalContext* globalCtx) {
if ((this->actor.colChkInfo.health != 0) || (this->actionFunc == EnSt_FinishBouncing)) {
if (DECR(this->gaveDamageSpinTimer) == 0) {
EnSt_SetCylinderOC(this, globalCtx);
}
DECR(this->invulnerableTimer);
DECR(this->takeDamageSpinTimer);
if (this->invulnerableTimer == 0 && this->takeDamageSpinTimer == 0) {
EnSt_SetBodyCylinderAC(this, globalCtx);
EnSt_SetLegsCylinderAC(this, globalCtx);
}
}
}
s32 EnSt_CheckHitLink(EnSt* this, GlobalContext* globalCtx) {
Player* player = GET_PLAYER(globalCtx);
s32 hit;
s32 i;
for (i = 0, hit = 0; i < 3; i++) {
if (((this->colCylinder[i + 3].base.ocFlags2 & OC2_HIT_PLAYER) != 0) == 0) {
continue;
}
this->colCylinder[i + 3].base.ocFlags2 &= ~OC2_HIT_PLAYER;
hit = true;
}
if (!hit) {
return false;
}
if (this->swayTimer == 0) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_ROLL);
}
this->gaveDamageSpinTimer = 30;
globalCtx->damagePlayer(globalCtx, -8);
Audio_PlayActorSound2(&player->actor, NA_SE_PL_BODY_HIT);
func_8002F71C(globalCtx, &this->actor, 4.0f, this->actor.yawTowardsPlayer, 6.0f);
return true;
}
s32 EnSt_CheckHitFrontside(EnSt* this) {
u8 acFlags = this->colCylinder[2].base.acFlags;
if (!!(acFlags & AC_HIT) == 0) {
// not hit
return false;
} else {
this->colCylinder[2].base.acFlags &= ~AC_HIT;
this->invulnerableTimer = 8;
this->playSwayFlag = 0;
this->swayTimer = 60;
return true;
}
}
s32 EnSt_CheckHitBackside(EnSt* this, GlobalContext* globalCtx) {
ColliderCylinder* cyl = &this->colCylinder[0];
s32 flags = 0; // ac hit flags from colliders 0 and 1
s32 hit = false;
if (cyl->base.acFlags & AC_HIT) {
cyl->base.acFlags &= ~AC_HIT;
hit = true;
flags |= cyl->info.acHitInfo->toucher.dmgFlags;
}
cyl = &this->colCylinder[1];
if (cyl->base.acFlags & AC_HIT) {
cyl->base.acFlags &= ~AC_HIT;
hit = true;
flags |= cyl->info.acHitInfo->toucher.dmgFlags;
}
if (!hit) {
return false;
}
this->invulnerableTimer = 8;
if (this->actor.colChkInfo.damageEffect == 1) {
if (this->stunTimer == 0) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_GOMA_JR_FREEZE);
this->stunTimer = 120;
Actor_SetColorFilter(&this->actor, 0, 0xC8, 0, this->stunTimer);
}
return false;
}
this->swayTimer = this->stunTimer = 0;
this->gaveDamageSpinTimer = 1;
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_3);
this->takeDamageSpinTimer = this->skelAnime.animLength;
Actor_SetColorFilter(&this->actor, 0x4000, 0xC8, 0, this->takeDamageSpinTimer);
if (Actor_ApplyDamage(&this->actor)) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_DAMAGE);
return false;
}
Enemy_StartFinishingBlow(globalCtx, &this->actor);
this->actor.flags &= ~ACTOR_FLAG_0;
this->groundBounces = 3;
this->deathTimer = 20;
this->actor.gravity = -1.0f;
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALWALL_DEAD);
if (flags & 0x1F820) {
// arrow, fire arrow, ice arrow, light arrow,
// and three unknows, unused arrows?
EnSt_SetupAction(this, EnSt_Die);
this->finishDeathTimer = 8;
} else {
EnSt_SetupAction(this, EnSt_BounceAround);
}
return true;
}
/**
* Checks if the Skulltula's colliders have been hit, returns true if the hit has dealt damage to the Skulltula
*/
s32 EnSt_CheckColliders(EnSt* this, GlobalContext* globalCtx) {
if (EnSt_CheckHitFrontside(this)) {
// player has hit the front shield area of the Skulltula
return false;
}
if (globalCtx->actorCtx.unk_02 != 0) {
return true;
}
if (EnSt_CheckHitBackside(this, globalCtx)) {
// player has hit the backside of the Skulltula
return true;
}
if (this->stunTimer == 0 && this->takeDamageSpinTimer == 0) {
// check if the Skulltula has hit link.
EnSt_CheckHitLink(this, globalCtx);
}
return false;
}
void EnSt_SetColliderScale(EnSt* this) {
f32 scaleAmount = 1.0f;
f32 radius;
f32 height;
f32 yShift;
s32 i;
if (this->actor.params == 1) {
scaleAmount = 1.4f;
}
radius = this->colSph.elements[0].dim.modelSphere.radius;
radius *= scaleAmount;
this->colSph.elements[0].dim.modelSphere.radius = radius;
for (i = 0; i < 6; i++) {
yShift = this->colCylinder[i].dim.yShift;
radius = this->colCylinder[i].dim.radius;
height = this->colCylinder[i].dim.height;
yShift *= scaleAmount;
radius *= scaleAmount;
height *= scaleAmount;
this->colCylinder[i].dim.yShift = yShift;
this->colCylinder[i].dim.radius = radius;
this->colCylinder[i].dim.height = height;
}
Actor_SetScale(&this->actor, 0.04f * scaleAmount);
this->colliderScale = scaleAmount;
this->floorHeightOffset = 32.0f * scaleAmount;
}
s32 EnSt_SetTeethColor(EnSt* this, s16 redTarget, s16 greenTarget, s16 blueTarget, s16 minMaxStep) {
s16 red = this->teethR;
s16 green = this->teethG;
s16 blue = this->teethB;
minMaxStep = 255 / (s16)(0.6f * minMaxStep);
if (minMaxStep <= 0) {
minMaxStep = 1;
}
Math_SmoothStepToS(&red, redTarget, 1, minMaxStep, minMaxStep);
Math_SmoothStepToS(&green, greenTarget, 1, minMaxStep, minMaxStep);
Math_SmoothStepToS(&blue, blueTarget, 1, minMaxStep, minMaxStep);
this->teethR = red;
this->teethG = green;
this->teethB = blue;
return 1;
}
s32 EnSt_DecrStunTimer(EnSt* this) {
if (this->stunTimer == 0) {
return 0;
}
this->stunTimer--; //! @bug no return but v0 ends up being stunTimer before decrement
}
/**
* Updates the yaw of the Skulltula, used for the shaking animation right before
* turning, and the actual turning to face away from the player, and then back to
* face the player
*/
void EnSt_UpdateYaw(EnSt* this, GlobalContext* globalCtx) {
u16 yawDir = 0;
Vec3s rot;
s16 yawDiff;
s16 timer;
s16 yawTarget;
// Shake towards the end of the stun.
if (this->stunTimer != 0) {
if (this->stunTimer < 30) {
if ((this->stunTimer % 2) != 0) {
this->actor.shape.rot.y += 0x800;
} else {
this->actor.shape.rot.y -= 0x800;
}
}
return;
}
if (this->swayTimer == 0 && this->deathTimer == 0 && this->finishDeathTimer == 0) {
// not swaying or dying
if (this->takeDamageSpinTimer != 0 || this->gaveDamageSpinTimer != 0) {
// Skulltula is doing a spinning animation
this->actor.shape.rot.y += 0x2000;
return;
}
if (this->actionFunc != EnSt_WaitOnGround) {
// set the timers to turn away or turn towards the player
this->rotAwayTimer = 30;
this->rotTowardsTimer = 0;
}
if (this->rotAwayTimer != 0) {
// turn away from the player
this->rotAwayTimer--;
if (this->rotAwayTimer == 0) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_ROLL);
this->rotTowardsTimer = 30;
}
} else if (this->rotTowardsTimer != 0) {
// turn towards the player
this->rotTowardsTimer--;
if (this->rotTowardsTimer == 0) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_ROLL);
this->rotAwayTimer = 30;
}
yawDir = 0x8000;
}
// calculate the new yaw to or away from the player.
rot = this->actor.shape.rot;
yawTarget = (this->actionFunc == EnSt_WaitOnGround ? this->actor.yawTowardsPlayer : this->initalYaw);
yawDiff = rot.y - (yawTarget ^ yawDir);
if (ABS(yawDiff) <= 0x4000) {
Math_SmoothStepToS(&rot.y, yawTarget ^ yawDir, 4, 0x2000, 1);
} else {
rot.y += 0x2000;
}
this->actor.shape.rot = this->actor.world.rot = rot;
// Do the shaking animation.
if (yawDir == 0 && this->rotAwayTimer < 0xA) {
timer = this->rotAwayTimer;
} else if (yawDir == 0x8000 && this->rotTowardsTimer < 0xA) {
timer = this->rotTowardsTimer;
} else {
return;
}
if ((timer % 2) != 0) {
this->actor.shape.rot.y += 0x800;
} else {
this->actor.shape.rot.y -= 0x800;
}
}
}
/**
* Checks to see if the Skulltula is done bouncing on the ground,
* spawns dust particles as the Skulltula hits the ground
*/
s32 EnSt_IsDoneBouncing(EnSt* this, GlobalContext* globalCtx) {
if (this->actor.velocity.y > 0.0f || this->groundBounces == 0) {
// the Skulltula is moving upwards or the groundBounces is 0
return false;
}
if (!(this->actor.bgCheckFlags & 1)) {
// the Skulltula is not on the ground.
return false;
}
Audio_PlayActorSound2(&this->actor, NA_SE_EN_DODO_M_GND);
EnSt_SpawnDust(this, globalCtx, 10);
// creates an elastic bouncing effect, boucing up less for each hit on the ground.
this->actor.velocity.y = 6.0f / (4 - this->groundBounces);
this->groundBounces--;
if (this->groundBounces != 0) {
return false;
} else {
// make sure the Skulltula stays on the ground.
this->actor.velocity.y = 0.0f;
}
return true;
}
void EnSt_Bob(EnSt* this, GlobalContext* globalCtx) {
f32 ySpeedTarget = 0.5f;
if ((globalCtx->state.frames & 8) != 0) {
ySpeedTarget *= -1.0f;
}
Math_SmoothStepToF(&this->actor.velocity.y, ySpeedTarget, 0.4f, 1000.0f, 0.0f);
}
s32 EnSt_IsCloseToPlayer(EnSt* this, GlobalContext* globalCtx) {
Player* player = GET_PLAYER(globalCtx);
f32 yDist;
if (this->takeDamageSpinTimer != 0) {
// skull is spinning from damage.
return false;
} else if (this->actor.xzDistToPlayer > 160.0f) {
// player is more than 160 xz units from the Skulltula
return false;
}
yDist = this->actor.world.pos.y - player->actor.world.pos.y;
if (yDist < 0.0f || yDist > 400.0f) {
// player is above the Skulltula or more than 400 units below
// the Skulltula
return false;
}
if (player->actor.world.pos.y < this->actor.floorHeight) {
// player is below the Skulltula's ground position
return false;
}
return true;
}
s32 EnSt_IsCloseToInitalPos(EnSt* this) {
f32 velY = this->actor.velocity.y;
f32 checkY = this->actor.world.pos.y + (velY * 2.0f);
if (checkY >= this->actor.home.pos.y) {
return true;
}
return false;
}
s32 EnSt_IsCloseToGround(EnSt* this) {
f32 velY = this->actor.velocity.y;
f32 checkY = this->actor.world.pos.y + (velY * 2.0f);
if (checkY - this->actor.floorHeight <= this->floorHeightOffset) {
return true;
}
return false;
}
/**
* Does the animation of the Skulltula swaying back and forth after the Skulltula
* has been hit in the front by a sword
*/
void EnSt_Sway(EnSt* this) {
Vec3f amtToTranslate;
Vec3f translatedPos;
f32 swayAmt;
s16 rotAngle;
if (this->swayTimer != 0) {
this->swayAngle += 0xA28;
this->swayTimer--;
if (this->swayTimer == 0) {
this->swayAngle = 0;
}
swayAmt = this->swayTimer * (7.0f / 15.0f);
rotAngle = Math_SinS(this->swayAngle) * (swayAmt * (65536.0f / 360.0f));
if (this->absPrevSwayAngle >= ABS(rotAngle) && this->playSwayFlag == 0) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_WAVE);
this->playSwayFlag = 1;
}
if (this->absPrevSwayAngle < ABS(rotAngle)) {
this->playSwayFlag = 0;
}
this->absPrevSwayAngle = ABS(rotAngle);
amtToTranslate.x = Math_SinS(rotAngle) * -200.0f;
amtToTranslate.y = Math_CosS(rotAngle) * -200.0f;
amtToTranslate.z = 0.0f;
Matrix_Push();
Matrix_Translate(this->ceilingPos.x, this->ceilingPos.y, this->ceilingPos.z, MTXMODE_NEW);
Matrix_RotateY(this->actor.world.rot.y * (M_PI / 32768.0f), MTXMODE_APPLY);
Matrix_MultVec3f(&amtToTranslate, &translatedPos);
Matrix_Pop();
this->actor.shape.rot.z = -(rotAngle * 2);
this->actor.world.pos.x = translatedPos.x;
this->actor.world.pos.z = translatedPos.z;
}
}
void EnSt_Init(Actor* thisx, GlobalContext* globalCtx) {
EnSt* this = (EnSt*)thisx;
s32 pad;
ActorShape_Init(&this->actor.shape, 0.0f, ActorShadow_DrawCircle, 14.0f);
SkelAnime_Init(globalCtx, &this->skelAnime, &object_st_Skel_005298, NULL, this->jointTable, this->morphTable, 30);
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_0);
this->blureIdx = EnSt_CreateBlureEffect(globalCtx);
EnSt_InitColliders(this, globalCtx);
if (thisx->params == 2) {
this->actor.flags |= ACTOR_FLAG_7;
}
if (this->actor.params == 1) {
this->actor.naviEnemyId = 0x05;
} else {
this->actor.naviEnemyId = 0x04;
}
EnSt_CheckCeilingPos(this, globalCtx);
this->actor.flags |= ACTOR_FLAG_14;
this->actor.flags |= ACTOR_FLAG_24;
EnSt_SetColliderScale(this);
this->actor.gravity = 0.0f;
this->initalYaw = this->actor.world.rot.y;
EnSt_SetupAction(this, EnSt_StartOnCeilingOrGround);
}
void EnSt_Destroy(Actor* thisx, GlobalContext* globalCtx) {
EnSt* this = (EnSt*)thisx;
s32 i;
Effect_Delete(globalCtx, this->blureIdx);
for (i = 0; i < 6; i++) {
Collider_DestroyCylinder(globalCtx, &this->colCylinder[i]);
}
Collider_DestroyJntSph(globalCtx, &this->colSph);
}
void EnSt_WaitOnCeiling(EnSt* this, GlobalContext* globalCtx) {
if (EnSt_IsCloseToPlayer(this, globalCtx)) {
EnSt_SetDropAnimAndVel(this);
EnSt_SetupAction(this, EnSt_MoveToGround);
} else {
EnSt_Bob(this, globalCtx);
}
}
/**
* Skulltula is waiting on the ground for the player to move away, or for
* a collider to have contact
*/
void EnSt_WaitOnGround(EnSt* this, GlobalContext* globalCtx) {
if (this->takeDamageSpinTimer != 0) {
this->takeDamageSpinTimer--;
if (this->takeDamageSpinTimer == 0) {
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_3);
}
}
if (this->animFrames != 0) {
this->animFrames--;
if (this->animFrames == 0) {
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_3);
}
}
if (!EnSt_IsCloseToPlayer(this, globalCtx)) {
// Player is no longer within range, return to ceiling.
EnSt_SetReturnToCeilingAnimation(this);
EnSt_SetupAction(this, EnSt_ReturnToCeiling);
return;
}
if (DECR(this->sfxTimer) == 0) {
// play the "laugh" sfx every 64 frames.
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_LAUGH);
this->sfxTimer = 64;
}
// simply bob up and down.
EnSt_Bob(this, globalCtx);
}
void EnSt_LandOnGround(EnSt* this, GlobalContext* globalCtx) {
if (this->animFrames != 0) {
this->animFrames--;
if (this->animFrames == 0) {
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_3);
}
}
if (this->takeDamageSpinTimer != 0) {
this->takeDamageSpinTimer--;
if (this->takeDamageSpinTimer == 0) {
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_3);
}
}
this->sfxTimer++;
if (this->sfxTimer == 14) {
// play the sound effect of the Skulltula hitting the ground.
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_DOWN_SET);
}
if ((this->actor.floorHeight + this->floorHeightOffset) < this->actor.world.pos.y) {
// the Skulltula has hit the ground.
this->sfxTimer = 0;
EnSt_SetupAction(this, EnSt_WaitOnGround);
} else {
Math_SmoothStepToF(&this->actor.velocity.y, 2.0f, 0.3f, 1.0f, 0.0f);
}
}
void EnSt_MoveToGround(EnSt* this, GlobalContext* globalCtx) {
if (this->takeDamageSpinTimer != 0) {
this->takeDamageSpinTimer--;
if (this->takeDamageSpinTimer == 0) {
Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, ENST_ANIM_5);
}
}
if (!EnSt_IsCloseToPlayer(this, globalCtx)) {
// the player moved out of range, return to the ceiling.
EnSt_SetReturnToCeilingAnimation(this);
EnSt_SetupAction(this, EnSt_ReturnToCeiling);
} else if (EnSt_IsCloseToGround(this)) {
// The Skulltula has become close to the ground.
EnSt_SpawnBlastEffect(this, globalCtx);
EnSt_SetLandAnimation(this);
EnSt_SetupAction(this, EnSt_LandOnGround);
} else if (DECR(this->sfxTimer) == 0) {
Audio_PlayActorSound2(&this->actor, NA_SE_EN_STALTU_DOWN);
this->sfxTimer = 3;
}
}
void EnSt_ReturnToCeiling(EnSt* this, GlobalContext* globalCtx) {
f32 animPctDone = this->skelAnime.curFrame / (this->skelAnime.animLength - 1.0f);
if (animPctDone == 1.0f) {
EnSt_SetReturnToCeilingAnimation(this);
}
if (EnSt_IsCloseToPlayer(this, globalCtx)) {
// player came back into range
EnSt_SetDropAnimAndVel(this);
EnSt_SetupAction(this, EnSt_MoveToGround);
} else if (EnSt_IsCloseToInitalPos(this)) {
// the Skulltula is close to the initial postion.
EnSt_SetWaitingAnimation(this);
EnSt_SetupAction(this, EnSt_WaitOnCeiling);
} else {
// accelerate based on the current animation frame.
this->actor.velocity.y = 4.0f * animPctDone;
}
}
/**
* The Skulltula has been killed, bounce around
*/
void EnSt_BounceAround(EnSt* this, GlobalContext* globalCtx) {
this->actor.colorFilterTimer = this->deathTimer;
func_8002D868(&this->actor);
this->actor.world.rot.x += 0x800;
this->actor.world.rot.z -= 0x800;
this->actor.shape.rot = this->actor.world.rot;
if (EnSt_IsDoneBouncing(this, globalCtx)) {
this->actor.shape.yOffset = 400.0f;
this->actor.speedXZ = 1.0f;
this->actor.gravity = -2.0f;
EnSt_SetupAction(this, EnSt_FinishBouncing);
} else {
Math_SmoothStepToF(&this->actor.shape.yOffset, 400.0f, 0.4f, 10000.0f, 0.0f);
}
}
/**
* Finish up the bouncing animation, and rotate towards the final position
*/
void EnSt_FinishBouncing(EnSt* this, GlobalContext* globalCtx) {
Vec3f zeroVec = { 0.0f, 0.0f, 0.0f };
if (DECR(this->deathTimer) == 0) {
this->actor.velocity = zeroVec;
this->finishDeathTimer = 8;
EnSt_SetupAction(this, EnSt_Die);
return;
}
if (DECR(this->setTargetYawTimer) == 0) {
this->deathYawTarget = Math_Vec3f_Yaw(&this->actor.world.pos, &this->actor.home.pos);
this->setTargetYawTimer = 8;
}
Math_SmoothStepToS(&this->actor.world.rot.x, 0x3FFC, 4, 0x2710, 1);
Math_SmoothStepToS(&this->actor.world.rot.z, 0, 4, 0x2710, 1);
Math_SmoothStepToS(&this->actor.world.rot.y, this->deathYawTarget, 0xA, 0x2710, 1);
this->actor.shape.rot = this->actor.world.rot;
func_8002D868(&this->actor);
this->groundBounces = 2;
EnSt_IsDoneBouncing(this, globalCtx);
}
/**
* Spawn the enemy dying effects, and drop a random item
*/
void EnSt_Die(EnSt* this, GlobalContext* globalCtx) {
if (DECR(this->finishDeathTimer) != 0) {
EnSt_SpawnDeadEffect(this, globalCtx);
} else {
Item_DropCollectibleRandom(globalCtx, NULL, &this->actor.world.pos, 0xE0);
Actor_Kill(&this->actor);
}
}
void EnSt_StartOnCeilingOrGround(EnSt* this, GlobalContext* globalCtx) {
if (!EnSt_IsCloseToGround(this)) {
this->rotAwayTimer = 60;
EnSt_SetupAction(this, EnSt_WaitOnCeiling);
EnSt_WaitOnCeiling(this, globalCtx);
} else {
EnSt_SetLandAnimation(this);
EnSt_SetupAction(this, EnSt_LandOnGround);
EnSt_LandOnGround(this, globalCtx);
}
}
void EnSt_Update(Actor* thisx, GlobalContext* globalCtx) {
EnSt* this = (EnSt*)thisx;
s32 pad;
Color_RGBA8 color = { 0, 0, 0, 0 };
if (this->actor.flags & ACTOR_FLAG_15) {
SkelAnime_Update(&this->skelAnime);
} else if (!EnSt_CheckColliders(this, globalCtx)) {
// no collision has been detected.
if (this->stunTimer == 0) {
SkelAnime_Update(&this->skelAnime);
}
if (this->swayTimer == 0 && this->stunTimer == 0) {
func_8002D7EC(&this->actor);
}
Actor_UpdateBgCheckInfo(globalCtx, &this->actor, 0.0f, 0.0f, 0.0f, 4);
if ((this->stunTimer == 0) && (this->swayTimer == 0)) {
// run the current action if the Skulltula isn't stunned
// or swaying.
this->actionFunc(this, globalCtx);
} else if (this->stunTimer != 0) {
// decrement the stun timer.
EnSt_DecrStunTimer(this);
} else {
// sway the Skulltula.
EnSt_Sway(this);
}
EnSt_UpdateYaw(this, globalCtx);
if (this->actionFunc == EnSt_WaitOnGround) {
if ((globalCtx->state.frames & 0x10) != 0) {
color.r = 255;
}
}
EnSt_SetTeethColor(this, color.r, color.g, color.b, 8);
EnSt_UpdateCylinders(this, globalCtx);
Actor_SetFocus(&this->actor, 0.0f);
}
}
s32 EnSt_OverrideLimbDraw(GlobalContext* globalCtx, s32 limbIndex, Gfx** dListP, Vec3f* pos, Vec3s* rot, void* thisx) {
EnSt* this = (EnSt*)thisx;
OPEN_DISPS(globalCtx->state.gfxCtx);
switch (limbIndex) {
case 1:
if (this->gaveDamageSpinTimer != 0 && this->swayTimer == 0) {
if (this->gaveDamageSpinTimer >= 2) {
EnSt_AddBlurVertex(this);
} else {
EnSt_AddBlurSpace(this);
}
}
break;
case 4:
// teeth
gDPPipeSync(POLY_OPA_DISP++);
gDPSetEnvColor(POLY_OPA_DISP++, this->teethR, this->teethG, this->teethB, 0);
break;
}
CLOSE_DISPS(globalCtx->state.gfxCtx);
return false;
}
void EnSt_PostLimbDraw(GlobalContext* globalCtx, s32 limbIndex, Gfx** dListP, Vec3s* rot, void* thisx) {
EnSt* this = (EnSt*)thisx;
Collider_UpdateSpheres(limbIndex, &this->colSph);
}
void EnSt_Draw(Actor* thisx, GlobalContext* globalCtx) {
EnSt* this = (EnSt*)thisx;
EnSt_CheckBodyStickHit(this, globalCtx);
func_80093D18(globalCtx->state.gfxCtx);
SkelAnime_DrawOpa(globalCtx, this->skelAnime.skeleton, this->skelAnime.jointTable, EnSt_OverrideLimbDraw,
EnSt_PostLimbDraw, this);
}