sm64pc/src/game/behaviors/pokey.inc.c

307 lines
12 KiB
C

/**
* Behavior for bhvPokey and bhvPokeyBodyPart.
* bhvPokey is responsible for the behavior of the pokey itself, as well as
* spawning the body parts.
* Pokey comes before its body parts in processing order, and the body parts
* are processed top to bottom.
*/
/**
* Hitbox for a single pokey body part.
*/
static struct ObjectHitbox sPokeyBodyPartHitbox = {
/* interactType: */ INTERACT_BOUNCE_TOP,
/* downOffset: */ 10,
/* damageOrCoinValue: */ 2,
/* health: */ 0,
/* numLootCoins: */ 0,
/* radius: */ 40,
/* height: */ 20,
/* hurtboxRadius: */ 20,
/* hurtboxHeight: */ 20,
};
/**
* Attack handlers for pokey body part.
*/
static u8 sPokeyBodyPartAttackHandlers[] = {
/* ATTACK_PUNCH: */ ATTACK_HANDLER_KNOCKBACK,
/* ATTACK_KICK_OR_TRIP: */ ATTACK_HANDLER_KNOCKBACK,
/* ATTACK_FROM_ABOVE: */ ATTACK_HANDLER_SQUISHED,
/* ATTACK_GROUND_POUND_OR_TWIRL: */ ATTACK_HANDLER_SQUISHED,
/* ATTACK_FAST_ATTACK: */ ATTACK_HANDLER_KNOCKBACK,
/* ATTACK_FROM_BELOW: */ ATTACK_HANDLER_KNOCKBACK,
};
/**
* Update function for pokey body part.
* The behavior parameter is the body part's index from 0 to 4, with 0 at the
* top.
*/
void bhv_pokey_body_part_update(void) {
// PARTIAL_UPDATE
s16 offsetAngle;
f32 baseHeight;
if (obj_update_standard_actions(3.0f)) {
if (o->parentObj->oAction == POKEY_ACT_UNLOAD_PARTS) {
obj_mark_for_deletion(o);
} else {
cur_obj_update_floor_and_walls();
obj_update_blinking(&o->oPokeyBodyPartBlinkTimer, 30, 60, 4);
// If the body part above us is dead, then decrease body part index
// by one, since new parts are spawned from the bottom.
//! It is possible to briefly get two body parts to have the same
// index by killing two body parts on the frame before a new part
// spawns, but one of the body parts shifts upward immediately,
// so not very interesting
if (o->oBehParams2ndByte > 1
&& !(o->parentObj->oPokeyAliveBodyPartFlags & (1 << (o->oBehParams2ndByte - 1)))) {
o->parentObj->oPokeyAliveBodyPartFlags =
o->parentObj->oPokeyAliveBodyPartFlags | 1 << (o->oBehParams2ndByte - 1);
o->parentObj->oPokeyAliveBodyPartFlags =
o->parentObj->oPokeyAliveBodyPartFlags & ((1 << o->oBehParams2ndByte) ^ ~0);
o->oBehParams2ndByte -= 1;
}
// Set the bottom body part size, and gradually increase it.
//! This "else if" means that if a body part above the expanding
// one dies, then the expanding will pause for one frame.
//! If you kill a body part as it's expanding, the body part that
// was above it will instantly shrink and begin expanding in its
// place.
else if (o->parentObj->oPokeyBottomBodyPartSize < 1.0f
&& o->oBehParams2ndByte + 1 == o->parentObj->oPokeyNumAliveBodyParts) {
approach_f32_ptr(&o->parentObj->oPokeyBottomBodyPartSize, 1.0f, 0.1f);
cur_obj_scale(o->parentObj->oPokeyBottomBodyPartSize * 3.0f);
}
//! Pausing causes jumps in offset angle
offsetAngle = o->oBehParams2ndByte * 0x4000 + gGlobalTimer * 0x800;
o->oPosX = o->parentObj->oPosX + coss(offsetAngle) * 6.0f;
o->oPosZ = o->parentObj->oPosZ + sins(offsetAngle) * 6.0f;
// This is the height of the tower beneath the body part
baseHeight = o->parentObj->oPosY
+ (120 * (o->parentObj->oPokeyNumAliveBodyParts - o->oBehParams2ndByte) - 240)
+ 120.0f * o->parentObj->oPokeyBottomBodyPartSize;
// We treat the base height as a minimum height, allowing the body
// part to briefly stay in the air after a part below it dies
if (o->oPosY < baseHeight) {
o->oPosY = baseHeight;
o->oVelY = 0.0f;
}
// Only the head has loot coins
if (o->oBehParams2ndByte == 0) {
o->oNumLootCoins = 1;
} else {
o->oNumLootCoins = 0;
}
// If the body part was attacked, then die. If the head was killed,
// then die after a delay.
if (obj_handle_attacks(&sPokeyBodyPartHitbox, o->oAction, sPokeyBodyPartAttackHandlers)) {
o->parentObj->oPokeyNumAliveBodyParts -= 1;
if (o->oBehParams2ndByte == 0) {
o->parentObj->oPokeyHeadWasKilled = TRUE;
// Last minute change to blue coins - not sure why they didn't
// just set it to -1 above
o->oNumLootCoins = -1;
}
o->parentObj->oPokeyAliveBodyPartFlags =
o->parentObj->oPokeyAliveBodyPartFlags & ((1 << o->oBehParams2ndByte) ^ ~0);
} else if (o->parentObj->oPokeyHeadWasKilled) {
cur_obj_become_intangible();
if (--o->oPokeyBodyPartDeathDelayAfterHeadKilled < 0) {
o->parentObj->oPokeyNumAliveBodyParts -= 1;
obj_die_if_health_non_positive();
}
} else {
// Die in order from top to bottom
// If a new body part spawns after the head has been killed, its
// death delay will be 0
o->oPokeyBodyPartDeathDelayAfterHeadKilled = (o->oBehParams2ndByte << 2) + 20;
}
cur_obj_move_standard(-78);
}
} else {
o->oAnimState = 1;
}
o->oGraphYOffset = o->header.gfx.scale[1] * 22.0f;
}
/**
* When mario gets within range, spawn the 5 body parts and enter the wander
* action.
*/
static void pokey_act_uninitialized(void) {
struct Object *bodyPart;
s32 i;
s16 partModel;
if (o->oDistanceToMario < 2000.0f) {
partModel = MODEL_POKEY_HEAD;
for (i = 0; i < 5; i++) {
// Spawn body parts at y offsets 480, 360, 240, 120, 0
// behavior param 0 = head, 4 = lowest body part
bodyPart = spawn_object_relative(i, 0, -i * 120 + 480, 0, o, partModel, bhvPokeyBodyPart);
if (bodyPart != NULL) {
obj_scale(bodyPart, 3.0f);
}
partModel = MODEL_POKEY_BODY_PART;
}
o->oPokeyAliveBodyPartFlags = 0x1F;
o->oPokeyNumAliveBodyParts = 5;
o->oPokeyBottomBodyPartSize = 1.0f;
o->oAction = POKEY_ACT_WANDER;
}
}
/**
* Wander around, replenishing body parts if they are killed. When mario moves
* far away, enter the unload parts action.
* While wandering, if mario is within 2000 units, try to move toward him. But
* if mario gets too close, then shy away from him.
*/
static void pokey_act_wander(void) {
s32 targetAngleOffset;
struct Object *bodyPart;
if (o->oPokeyNumAliveBodyParts == 0) {
obj_mark_for_deletion(o);
} else if (o->oDistanceToMario > 2500.0f) {
o->oAction = POKEY_ACT_UNLOAD_PARTS;
o->oForwardVel = 0.0f;
} else {
treat_far_home_as_mario(1000.0f);
cur_obj_update_floor_and_walls();
if (o->oPokeyHeadWasKilled) {
o->oForwardVel = 0.0f;
} else {
o->oForwardVel = 5.0f;
// If a body part is missing, replenish it after 100 frames
if (o->oPokeyNumAliveBodyParts < 5) {
if (o->oTimer > 100) {
// Because the body parts shift index whenever a body part
// is killed, the new part's index is equal to the number
// of living body parts
bodyPart = spawn_object_relative(o->oPokeyNumAliveBodyParts, 0, 0, 0, o,
MODEL_POKEY_BODY_PART, bhvPokeyBodyPart);
if (bodyPart != NULL) {
o->oPokeyAliveBodyPartFlags =
o->oPokeyAliveBodyPartFlags | (1 << o->oPokeyNumAliveBodyParts);
o->oPokeyNumAliveBodyParts += 1;
o->oPokeyBottomBodyPartSize = 0.0f;
obj_scale(bodyPart, 0.0f);
}
o->oTimer = 0;
}
} else {
o->oTimer = 0;
}
if (o->oPokeyTurningAwayFromWall) {
o->oPokeyTurningAwayFromWall =
obj_resolve_collisions_and_turn(o->oPokeyTargetYaw, 0x200);
} else {
// If far from home, turn back toward home
if (o->oDistanceToMario >= 25000.0f) {
o->oPokeyTargetYaw = o->oAngleToMario;
}
if (!(o->oPokeyTurningAwayFromWall =
obj_bounce_off_walls_edges_objects(&o->oPokeyTargetYaw))) {
if (o->oPokeyChangeTargetTimer != 0) {
o->oPokeyChangeTargetTimer -= 1;
} else if (o->oDistanceToMario > 2000.0f) {
o->oPokeyTargetYaw = obj_random_fixed_turn(0x2000);
o->oPokeyChangeTargetTimer = random_linear_offset(30, 50);
} else {
// The goal of this computation is to make pokey approach
// mario directly if he is far away, but to shy away from
// him when he is nearby
// targetAngleOffset is 0 when distance to mario is >= 1838.4
// and 0x4000 when distance to mario is <= 200
targetAngleOffset = (s32)(0x4000 - (o->oDistanceToMario - 200.0f) * 10.0f);
if (targetAngleOffset < 0) {
targetAngleOffset = 0;
} else if (targetAngleOffset > 0x4000) {
targetAngleOffset = 0x4000;
}
// If we need to rotate CCW to get to mario, then negate
// the target angle offset
if ((s16)(o->oAngleToMario - o->oMoveAngleYaw) > 0) {
targetAngleOffset = -targetAngleOffset;
}
// When mario is far, targetAngleOffset is 0, so he moves
// toward him directly. When mario is close,
// targetAngleOffset is 0x4000, so he turns 90 degrees
// away from mario
o->oPokeyTargetYaw = o->oAngleToMario + targetAngleOffset;
}
}
cur_obj_rotate_yaw_toward(o->oPokeyTargetYaw, 0x200);
}
}
cur_obj_move_standard(-78);
}
}
/**
* Move back to home and enter the uninitialized action.
* The pokey body parts check to see if pokey is in this action, and if so,
* unload themselves.
*/
static void pokey_act_unload_parts(void) {
o->oAction = POKEY_ACT_UNINITIALIZED;
cur_obj_set_pos_to_home();
}
/**
* Update function for pokey.
*/
void bhv_pokey_update(void) {
// PARTIAL_UPDATE
o->oDeathSound = SOUND_OBJ_POKEY_DEATH;
switch (o->oAction) {
case POKEY_ACT_UNINITIALIZED:
pokey_act_uninitialized();
break;
case POKEY_ACT_WANDER:
pokey_act_wander();
break;
case POKEY_ACT_UNLOAD_PARTS:
pokey_act_unload_parts();
break;
}
}