engine-psx/src/object_state_update.c
2024-12-22 17:48:02 -03:00

842 lines
33 KiB
C

#include <stdio.h>
#include <stdlib.h>
#include "object.h"
#include "object_state.h"
#include "player.h"
#include "collision.h"
#include "sound.h"
#include "camera.h"
#include "render.h"
#include "timer.h"
#include "screen.h"
#include "screens/level.h"
#define ANIM_WALKING 0x0854020e
// Extern elements
extern Player player;
extern Camera camera;
extern PlayerConstants CNST_SPEEDSHOES;
extern TileMap16 map16;
extern TileMap128 map128;
extern LevelData leveldata;
extern SoundEffect sfx_ring;
extern SoundEffect sfx_pop;
extern SoundEffect sfx_sprn;
extern SoundEffect sfx_chek;
extern SoundEffect sfx_death;
extern SoundEffect sfx_ringl;
extern SoundEffect sfx_shield;
extern SoundEffect sfx_yea;
extern SoundEffect sfx_switch;
extern SoundEffect sfx_bubble;
extern int debug_mode;
extern uint8_t level_ring_count;
extern uint32_t level_score_count;
extern uint8_t level_finished;
extern int32_t level_water_y;
// Object-specific definitions
#define RING_GRAVITY 0x00000180
// Update functions
static void _ring_update(ObjectState *state, ObjectTableEntry *typedata, VECTOR *pos);
static void _goal_sign_update(ObjectState *state, ObjectTableEntry *typedata, VECTOR *pos);
static void _monitor_update(ObjectState *state, ObjectTableEntry *typedata, VECTOR *pos);
static void _spring_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos, uint8_t is_red);
static void _spring_diagonal_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos, uint8_t is_red);
static void _checkpoint_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos);
static void _spikes_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos);
static void _explosion_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _monitor_image_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _shield_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _switch_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _bubble_patch_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
static void _bubble_update(ObjectState *state, ObjectTableEntry *, VECTOR *);
// Player hitbox information. Calculated once per frame.
static int32_t player_vx, player_vy; // Top left corner of player hitbox
static int32_t player_width = 16;
static int32_t player_height = HEIGHT_RADIUS_NORMAL << 1;
static uint8_t player_attacking;
int player_hitbox_shown;
void
_draw_player_hitbox()
{
if(player_hitbox_shown) return;
player_hitbox_shown = 1;
uint16_t
rel_vx = player_vx - (camera.pos.vx >> 12) + CENTERX,
rel_vy = player_vy - (camera.pos.vy >> 12) + CENTERY;
POLY_F4 *hitbox = get_next_prim();
increment_prim(sizeof(POLY_F4));
setPolyF4(hitbox);
setSemiTrans(hitbox, 1);
setXYWH(hitbox, rel_vx, rel_vy, 16, player_height);
setRGB0(hitbox, 0xfb, 0x94, 0xdc);
sort_prim(hitbox, OTZ_LAYER_OBJECTS);
}
void
object_update(ObjectState *state, ObjectTableEntry *typedata, VECTOR *pos)
{
if(state->props & OBJ_FLAG_DESTROYED) return;
// Calculate top left corner of player AABB.
// Note that player data is in fixed-point format!
player_vx = (player.pos.vx >> 12) - 8;
player_attacking = (player.action == ACTION_JUMPING ||
player.action == ACTION_ROLLING ||
player.action == ACTION_SPINDASH ||
player.action == ACTION_DROPDASH);
player_height = (player_attacking
? HEIGHT_RADIUS_ROLLING
: HEIGHT_RADIUS_NORMAL) << 1;
player_vy = (player.pos.vy >> 12) - (player_height >> 1) - 1;
if(debug_mode > 1) {
_draw_player_hitbox();
}
switch(state->id) {
default: break;
case OBJ_RING: _ring_update(state, typedata, pos); break;
case OBJ_GOAL_SIGN: _goal_sign_update(state, typedata, pos); break;
case OBJ_MONITOR: _monitor_update(state, typedata, pos); break;
case OBJ_SPRING_YELLOW: _spring_update(state, typedata, pos, 0); break;
case OBJ_SPRING_RED: _spring_update(state, typedata, pos, 1); break;
case OBJ_SPRING_YELLOW_DIAGONAL: _spring_diagonal_update(state, typedata, pos, 0); break;
case OBJ_SPRING_RED_DIAGONAL: _spring_diagonal_update(state, typedata, pos, 1); break;
case OBJ_CHECKPOINT: _checkpoint_update(state, typedata, pos); break;
case OBJ_SPIKES: _spikes_update(state, typedata, pos); break;
case OBJ_EXPLOSION: _explosion_update(state, typedata, pos); break;
case OBJ_MONITOR_IMAGE: _monitor_image_update(state, typedata, pos); break;
case OBJ_SHIELD: _shield_update(state, typedata, pos); break;
case OBJ_SWITCH: _switch_update(state, typedata, pos); break;
case OBJ_BUBBLE_PATCH: _bubble_patch_update(state, typedata, pos); break;
case OBJ_BUBBLE: _bubble_update(state, typedata, pos); break;
}
}
/* ======================== */
/* OBJECT-SPECIFIC IMPL */
/* ======================== */
static void
_ring_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
if(state->anim_state.animation == 0) {
// Calculate actual top left corner of ring AABB
pos->vx -= 8; pos->vy -= (8 + 32);
// Hey -- if this is a moving ring (ring loss behaviour), only
// allow the player to collect it if its action is not ACTION_HURT
if(!((state->props & OBJ_FLAG_RING_MOVING) && (player.action == ACTION_HURT))) {
// Ring collision
if(aabb_intersects(player_vx, player_vy, player_width, player_height,
pos->vx, pos->vy, 16, 16))
{
state->anim_state.animation = 1;
state->anim_state.frame = 0;
state->props ^= OBJ_FLAG_ANIM_LOCK; // Unlock from global timer
level_ring_count++;
sound_play_vag(sfx_ring, 0);
return;
}
}
// If ring is moving, we will not proceed to move it!
if(state->props & OBJ_FLAG_RING_MOVING) {
state->timer++;
// If the ring is way too far from camera, just destroy it
// By "too far", I mean two screens apart.
// Also, moving rings only live for 256 frames
if((state->freepos->vx < camera.pos.vx - (SCREEN_XRES << 13))
|| (state->freepos->vx > camera.pos.vx + (SCREEN_XRES << 13))
|| (state->freepos->vy < camera.pos.vy - (SCREEN_YRES << 13))
|| (state->freepos->vy > camera.pos.vy + (SCREEN_YRES << 13))
|| (state->timer >= 256)) {
state->props |= OBJ_FLAG_DESTROYED;
return;
}
// Apply gravity
state->freepos->spdy += RING_GRAVITY;
/* In the original Sonic The Hedgehog for Sega Genesis, ring bouncing
* only occurred every 4 frames and disregarded walls, all for
* performance reasons. But we don't have to do this here, since
* our MIPS processor is about 4.5 times faster than the Motorola 68k
* (~7.6MHz vs ~33.87MHz).
* So we're checking for ground and wall collisions every frame.
*/
if(/*!(state->timer % 4) &&*/ state->freepos->spdy > 0) {
// Use Sonic's own linecast algorithm, since it is aware of
// level geometry -- and oh, level data is stored in external
// variables as well. Check the file header.
if(linecast(&leveldata, &map128, &map16,
pos->vx + 8, pos->vy + 8,
CDIR_FLOOR, 10, CDIR_FLOOR).collided)
// Multiply Y speed by -0.75
state->freepos->spdy = (state->freepos->spdy * -0x00000c00) >> 12;
}
if(/*!(state->timer % 4) &&*/
((state->freepos->spdx < 0) && linecast(&leveldata, &map128, &map16,
pos->vx + 8, pos->vy + 8,
CDIR_LWALL, 10, CDIR_FLOOR).collided)
|| ((state->freepos->spdx > 0) && linecast(&leveldata, &map128, &map16,
pos->vx + 8, pos->vy + 8,
CDIR_RWALL, 10, CDIR_FLOOR).collided))
// Multiply X speed by -0.75
state->freepos->spdx = (state->freepos->spdx * -0x00000c00) >> 12;
// Transform ring position wrt. speed
state->freepos->vx += state->freepos->spdx;
state->freepos->vy += state->freepos->spdy;
}
} else if(state->anim_state.animation == OBJ_ANIMATION_NO_ANIMATION
&& !(state->props & OBJ_FLAG_DESTROYED))
{
state->props |= OBJ_FLAG_DESTROYED;
}
}
void
_goal_sign_change_score()
{
// TODO: A temporary score count. Change this later!
level_score_count += level_ring_count * 100;
uint32_t seconds = get_elapsed_frames() / 60;
if(seconds <= 29) level_score_count += 50000; // Under 0:30
else if(seconds <= 44) level_score_count += 10000; // Under 0:45
else if(seconds <= 59) level_score_count += 5000; // Under 1:00
else if(seconds <= 89) level_score_count += 4000; // Under 1:30
else if(seconds <= 119) level_score_count += 3000; // Under 2:00
else if(seconds <= 179) level_score_count += 2000; // Under 3:00
else if(seconds <= 239) level_score_count += 1000; // Under 4:00
else if(seconds <= 299) level_score_count += 500; // Under 5:00
// Otherwise you get nothing
}
static void
_goal_sign_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
if(state->anim_state.animation == 0) {
if(pos->vx <= (player.pos.vx >> 12)) {
state->anim_state.animation = 1;
state->anim_state.frame = 0;
camera_set_left_bound(&camera, ((pos->vx + 80) << 12));
state->timer = 180;
level_finished = 1;
pause_elapsed_frames();
}
} else if(state->anim_state.animation == 1) {
state->timer--;
if(state->timer < 0) {
// Set animation according to character
state->anim_state.animation = 2;
state->timer = 360; // 6-seconds music
sound_bgm_play(BGM_LEVELCLEAR);
_goal_sign_change_score();
}
} else if(state->anim_state.animation < OBJ_ANIMATION_NO_ANIMATION) {
state->timer--;
// Doesn't hurt to not allow the CD reader to go berserk
sound_bgm_check_stop(BGM_LEVELCLEAR);
if((state->timer < 0) && (screen_level_getstate() == 2)) {
screen_level_setstate(3);
} else if(screen_level_getstate() == 4) {
uint8_t lvl = screen_level_getlevel();
// TODO: This is temporary and goes only upto R2Z1
if(lvl < 6) {
if(lvl == 4) {
// TODO: Transition from GHZ1 to SWZ1
// THIS IS TEMPORARY
screen_level_setlevel(6);
} else screen_level_setlevel(lvl + 1);
scene_change(SCREEN_LEVEL);
} else {
scene_change(SCREEN_COMINGSOON);
}
}
}
}
static void
_monitor_update(ObjectState *state, ObjectTableEntry *entry, VECTOR *pos)
{
if(state->anim_state.animation == 0) {
// Calculate solidity
int32_t solidity_vx = pos->vx - 16;
int32_t solidity_vy = pos->vy - 32; // Monitor is a 32x32 solid box
int32_t hitbox_vx = pos->vx - 15;
int32_t hitbox_vy = pos->vy - 32; // Monitor hitbox is a 28x32 solid box
// Perform collision detection
if(aabb_intersects(player_vx, player_vy, player_width, player_height,
solidity_vx, solidity_vy, 32, 32))
{
if(aabb_intersects(player_vx, player_vy, player_width, player_height,
hitbox_vx, hitbox_vy, 28, 32)
&& player_attacking) {
state->anim_state.animation = 1;
state->anim_state.frame = 0;
state->frag_anim_state->animation = OBJ_ANIMATION_NO_ANIMATION;
level_score_count += 10;
sound_play_vag(sfx_pop, 0);
// Create monitor image object.
// Account for fragment offset as well.
PoolObject *image = object_pool_create(OBJ_MONITOR_IMAGE);
image->freepos.vx = (pos->vx << 12) + ((int32_t)entry->fragment->offsetx << 12);
image->freepos.vy = (pos->vy << 12) + ((int32_t)entry->fragment->offsety << 12);
image->state.anim_state.animation = ((MonitorExtra *)state->extra)->kind;
// Create explosion effect
PoolObject *explosion = object_pool_create(OBJ_EXPLOSION);
explosion->freepos.vx = (pos->vx << 12);
explosion->freepos.vy = (pos->vy << 12);
explosion->state.anim_state.animation = 0; // Small explosion
if(!player.grnd && player.vel.vy > 0) {
player.vel.vy *= -1;
}
} else {
// Landing on top
if(((player_vy + player_height) < solidity_vy + 16) &&
((player_vx >= solidity_vx - 8) && ((player_vx + 8) <= solidity_vx + 32)))
{
player.ev_grnd1.collided = player.ev_grnd2.collided = 1;
player.ev_grnd1.angle = player.ev_grnd2.angle = 0;
player.ev_grnd1.coord = player.ev_grnd2.coord = solidity_vy;
} else if((player_vy + 8) > solidity_vy) {
// Check for intersection on left/right
if((player_vx + 8) < pos->vx) {
player.ev_right.collided = 1;
player.ev_right.coord = (solidity_vx + 2);
player.ev_right.angle = 0;
} else {
player.ev_left.collided = 1;
player.ev_left.coord = solidity_vx + 16;
player.ev_right.angle = 0;
}
}
}
}
}
}
static void
_spring_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos, uint8_t is_red)
{
if(state->anim_state.animation == 0) {
int32_t solidity_vx = pos->vx - 16;
int32_t solidity_vy = pos->vy - 16; // Spring is 32x16 solid
int32_t solidity_w = 32;
int32_t solidity_h = 16;
if(state->flipmask & MASK_FLIP_ROTCW) {
solidity_vx = pos->vx - 32;
solidity_vy = pos->vy + 16;
solidity_w = 16;
solidity_h = 32;
} else if(state->flipmask & MASK_FLIP_ROTCT) {
solidity_vx = pos->vx - 48;
solidity_vy = pos->vy - 48;
solidity_w = 16;
solidity_h = 32;
} else if(state->flipmask & MASK_FLIP_FLIPY) {
solidity_vy -= 48;
}
ObjectCollision collision_side =
hitbox_collision(player_vx, player_vy, player_width, player_height,
solidity_vx, solidity_vy, solidity_w, solidity_h);
// Simple spring collision.
// In this case, springs are not solid, and the player's spring action
// relate to where the spring is pointing at.
if(collision_side == OBJ_SIDE_NONE)
return;
else if(state->flipmask & MASK_FLIP_ROTCT) { // Left-pointing spring
//player.pos.vx = (solidity_vx - player_width) << 12;
player.ev_right.collided = 0; // Detach player from right wall if needed
player.vel.vz = is_red ? -0x10000 : -0xa000;
if(!player.grnd) player.vel.vx = is_red ? -0x10000 : -0xa000;
player.ctrllock = 16;
player.anim_dir = -1;
state->anim_state.animation = 1;
sound_play_vag(sfx_sprn, 0);
} else if(state->flipmask & MASK_FLIP_ROTCW) { // Right-pointing spring
//player.pos.vx = (solidity_vx + solidity_w + player_width + 8) << 12;
player.ev_left.collided = 0; // Detach player from left wall if needed
player.vel.vz = is_red ? 0x10000 : 0xa000;
if(!player.grnd) player.vel.vx = is_red ? 0x10000 : 0xa000;
player.ctrllock = 16;
player.anim_dir = 1;
state->anim_state.animation = 1;
sound_play_vag(sfx_sprn, 0);
} else if(state->flipmask == 0) { // Top-pointing spring
player.pos.vy = (solidity_vy - (player_height >> 1)) << 12;
player.grnd = 0;
player.vel.vy = is_red ? -0x10000 : -0xa000;
player.angle = 0;
player.action = ACTION_SPRING;
state->anim_state.animation = 1;
sound_play_vag(sfx_sprn, 0);
} else if(state->flipmask & MASK_FLIP_FLIPY) { // Bottom-pointing spring
player.pos.vy = (solidity_vy + solidity_h + (player_height >> 1)) << 12;
player.grnd = 0;
player.vel.vy = is_red ? 0x10000 : 0xa000;
player.angle = 0;
player.action = ACTION_SPRING;
state->anim_state.animation = 1;
sound_play_vag(sfx_sprn, 0);
}
} else if(state->anim_state.animation == OBJ_ANIMATION_NO_ANIMATION) {
state->anim_state.animation = 0;
state->anim_state.frame = 0;
}
}
static void
_checkpoint_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
if(!(state->props & OBJ_FLAG_CHECKPOINT_ACTIVE)) {
int32_t hitbox_vx = pos->vx - 8;
int32_t hitbox_vy = pos->vy - 48;
if(aabb_intersects(player_vx, player_vy, player_width, player_height,
hitbox_vx, hitbox_vy, 16, 48))
{
state->props |= OBJ_FLAG_CHECKPOINT_ACTIVE;
state->frag_anim_state->animation = 1;
state->frag_anim_state->frame = 0;
sound_play_vag(sfx_chek, 0);
}
}
}
#define SPRND_ST_R 0x0000b500 // 11.3125
#define SPRND_ST_Y 0x00007120 // 7.0703125
static void
_spring_diagonal_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos, uint8_t is_red)
{
// For diagonal springs, interaction should occur if and only if the player
// is colliding with its top.
if(state->anim_state.animation == 0) {
int32_t solidity_vx = pos->vx - 16;
int32_t solidity_vy = pos->vy - 32; // Spring is 32x32 solid
int32_t solidity_w = 32;
int32_t solidity_h = 32;
// Spring hitbox is actually calculated relative to player's X position
// within it.
// The hitbox is always low on the bottom 10 pixels of the spring,
// unless the player is at any given X coordinate of it; if so, the
// hitbox height increases/decreases to give a diagonal effect.
// This way, the player will always collide with a hitbox that looks
// like a diagonal trapezium.
int32_t shrink = 0;
int32_t delta = 0;
if(state->flipmask & MASK_FLIP_FLIPX) {
delta = solidity_w - (player_vx - solidity_vx);
} else {
delta = player_vx - solidity_vx + 16;
}
if(delta > 10 && delta < 33) {
shrink = delta - 10;
} else shrink = 22;
solidity_h -= shrink;
if(state->flipmask & MASK_FLIP_FLIPY) {
solidity_vy -= 32;
} else solidity_vy += shrink;
ObjectCollision collision_side =
hitbox_collision(player_vx, player_vy, player_width, player_height,
solidity_vx, solidity_vy, solidity_w, solidity_h);
if(collision_side == OBJ_SIDE_NONE) return;
player.grnd = 0;
player.vel.vx = is_red ? SPRND_ST_R : SPRND_ST_Y;
player.vel.vy = is_red ? SPRND_ST_R : SPRND_ST_Y;
if(!(state->flipmask & MASK_FLIP_FLIPY)) player.vel.vy *= -1;
if(state->flipmask & MASK_FLIP_FLIPX) {
player.vel.vx *= -1;
player.anim_dir = -1; // Flip on X: point player left
} else player.anim_dir = 1; // No flip on X: point player right
player.angle = 0;
player.airdirlock = 1;
player.action = ACTION_SPRING;
state->anim_state.animation = 1;
sound_play_vag(sfx_sprn, 0);
} else if(state->anim_state.animation == OBJ_ANIMATION_NO_ANIMATION) {
state->anim_state.animation = 0;
state->anim_state.frame = 0;
}
}
static void
_spikes_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
// TODO: For now, only spikes pointing upwards check for collision
if(state->flipmask != 0) return;
// Collision logic is very similar to monitors
// Calculate solidity
int32_t solidity_vx = pos->vx - 16;
int32_t solidity_vy = pos->vy - 32; // Spikes are a 32x32 solid box
// Perform collision detection
if(aabb_intersects(player_vx, player_vy, player_width, player_height,
solidity_vx, solidity_vy, 32, 32))
{
// Landing on top
if(((player_vy + player_height) < solidity_vy + 16) &&
((player_vx >= solidity_vx - 8) && ((player_vx + 8) <= solidity_vx + 32)))
{
if(player.action != ACTION_HURT && player.iframes == 0) {
player_do_damage(&player, (solidity_vx + 16) << 12);
return;
}
player.ev_grnd1.collided = player.ev_grnd2.collided = 1;
player.ev_grnd1.angle = player.ev_grnd2.angle = 0;
player.ev_grnd1.coord = player.ev_grnd2.coord =
solidity_vy + (player_attacking
? (HEIGHT_RADIUS_NORMAL - HEIGHT_RADIUS_ROLLING)
: 0);
} else if((player_vy + 8) > solidity_vy) {
// Check for intersection on left/right
if((player_vx + 8) < pos->vx) {
player.ev_right.collided = 1;
player.ev_right.coord = (solidity_vx + 2);
player.ev_right.angle = 0;
} else {
player.ev_left.collided = 1;
player.ev_left.coord = solidity_vx + 16;
player.ev_right.angle = 0;
}
}
}
}
static void
_explosion_update(ObjectState *state, ObjectTableEntry *, VECTOR *)
{
// Explosions are simple particles: their animation is finished?
// If so, destroy.
if(state->anim_state.animation == OBJ_ANIMATION_NO_ANIMATION)
state->props |= OBJ_FLAG_DESTROYED;
}
static void
_monitor_image_update(ObjectState *state, ObjectTableEntry *, VECTOR *)
{
state->timer++;
PoolObject *newobj;
// Monitor images ascend for 15 frames, then stay still for 15 more
if(state->timer <= 15) state->freepos->vy -= (ONE << 1);
if(state->timer > 30) {
switch(state->anim_state.animation) {
case MONITOR_KIND_NONE:
sound_play_vag(sfx_death, 0);
break;
case MONITOR_KIND_1UP:
sound_play_vag(sfx_yea, 0);
break;
case MONITOR_KIND_RING:
sound_play_vag(sfx_ring, 0);
level_ring_count += 10;
break;
case MONITOR_KIND_SHIELD:
if(player.shield != 1) {
player.shield = 1;
newobj = object_pool_create(OBJ_SHIELD);
newobj->freepos.vx = player.pos.vx;
newobj->freepos.vy = player.pos.vy + (20 << 12);
}
sound_play_vag(sfx_shield, 0);
break;
case MONITOR_KIND_SPEEDSHOES:
// Start speed shoes count
player.speedshoes_frames = 1200; // 20 seconds
player.cnst = &CNST_SPEEDSHOES;
sound_bgm_play(BGM_SPEEDSHOES);
break;
default: break;
}
state->props |= OBJ_FLAG_DESTROYED;
}
}
static void
_shield_update(ObjectState *state, ObjectTableEntry *, VECTOR *)
{
// Just stay with the player and disappear if player gets hurt
if(player.shield != 1) {
state->props |= OBJ_FLAG_DESTROYED;
return;
}
state->freepos->vx = player.pos.vx;
state->freepos->vy = player.pos.vy + (16 << 12);
// Compensate position since it is drawn before player update
state->freepos->vx += player.vel.vx;
state->freepos->vy += player.vel.vy;
}
static void
_switch_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
// Switches are always solid at same size and change animation
// while being upon.
int32_t solidity_vx = pos->vx - 16;
int32_t solidity_vy = pos->vy - 8;
// Set as not pressed by default
state->anim_state.animation = 0;
if(aabb_intersects(player_vx, player_vy, player_width, player_height,
solidity_vx, solidity_vy, 32, 8))
{
// Check for intersection on left/right
if((player_vy + 36) > solidity_vy
&& !((player_vx >= solidity_vx - 8) && ((player_vx + 8) <= solidity_vx + 32))) {
if((player_vx + 8) < pos->vx) {
player.ev_right.collided = 1;
player.ev_right.coord = (solidity_vx + 2);
player.ev_right.angle = 0;
} else {
player.ev_left.collided = 1;
player.ev_left.coord = solidity_vx + 16;
player.ev_right.angle = 0;
}
}
// Landing on top; pressing
else if(((player_vy + player_height) < solidity_vy + 16))
{
player.ev_grnd1.collided = player.ev_grnd2.collided = 1;
player.ev_grnd1.angle = player.ev_grnd2.angle = 0;
player.ev_grnd1.coord = player.ev_grnd2.coord = solidity_vy;
state->anim_state.animation = 1;
if(!(state->props & OBJ_FLAG_SWITCH_PRESSED)) {
// Switch was just pressed; play "beep"
sound_play_vag(sfx_switch, 0);
}
state->props |= OBJ_FLAG_SWITCH_PRESSED;
}
} else {
// If button is pressed... un-press it
state->props &= ~OBJ_FLAG_SWITCH_PRESSED;
}
}
static void
_bubble_patch_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
// Fixed bubble patch sets
static const uint8_t bubble_size_sets[4][6] = {
{0, 0, 0, 0, 1, 0},
{0, 0, 0, 1, 0, 0},
{1, 0, 1, 0, 0, 0},
{0, 1, 0, 0, 1, 0},
};
BubblePatchExtra *extra = state->extra;
if(extra->timer > 0) extra->timer--;
if(extra->timer == 0) {
// Flip production state between idle/producing.
// Since num_bubbles is always 0 when idle, we use this as shortcut
if(extra->num_bubbles == 0) {
extra->state = (extra->state + 1) % 2;
if(extra->state == 0) {
// Flipped from producing to idle. Pick a random delay [128; 255]
extra->timer = 128 + (rand() % 128);
extra->cycle = (extra->cycle + 1) % (extra->frequency + 1);
extra->produced_big = 0;
} else {
// Flipped from idle to producing
extra->num_bubbles = 1 + (rand() % 6); // Random [1; 6] bubbles
extra->bubble_set = rand() % 4; // Random bubble set [0; 4]
extra->bubble_idx = 0;
extra->timer = rand() % 32; // Random [0; 31] frames
}
} else {
// Produce a bubble according to constants
PoolObject *bubble = object_pool_create(OBJ_BUBBLE);
if(bubble) {
bubble->state.anim_state.animation =
bubble_size_sets[extra->bubble_set][extra->bubble_idx++];
bubble->freepos.vx = (pos->vx << 12)
- (8 << 12)
+ ((rand() % 16) << 12);
bubble->freepos.vy = (pos->vy << 12);
// If this is a big bubble cycle, however, we may want to turn
// the current bubble into a big one
if((extra->cycle == extra->frequency) && !extra->produced_big) {
// Roll a dice with 1/4 of chance, but if this is a big
// bubble cycle and we're at the last bubble, make it big
// regardless!
if((extra->num_bubbles == 1) || !(rand() % 4)) {
extra->produced_big = 1;
bubble->state.anim_state.animation = 2;
}
}
}
extra->timer = rand() % 32; // Random [0; 31] frames
extra->num_bubbles--;
}
}
}
static void
_bubble_update(ObjectState *state, ObjectTableEntry *, VECTOR *pos)
{
// NOTE: this object can only exist as a free object. Do not insist.
// FIRST OFF: If way too far from camera, destroy it
if((state->freepos->vx < camera.pos.vx - (SCREEN_XRES << 13))
|| (state->freepos->vx > camera.pos.vx + (SCREEN_XRES << 13))
|| (state->freepos->vy < camera.pos.vy - (SCREEN_YRES << 13))
|| (state->freepos->vy > camera.pos.vy + (SCREEN_YRES << 13))
|| (state->timer >= 256)) {
state->props |= OBJ_FLAG_DESTROYED;
return;
}
// INITIALIZATION: if the bubble has no X speed, initialize it and
// set the timer
if(state->freepos->spdx == 0) {
// 8 pixels every 128 frames
// 0.0625 pixel per frame
state->freepos->spdx = 0x100;
state->timer = 128;
// Start with a 50% chance random direction
state->timer *= ((rand() % 2) * 2) - 1;
// If this is a number bubble, we need to initialize its position
// relative to screen center, because it will hang around at the same
// X and Y position on screen.
if(state->anim_state.animation >= 3) {
state->freepos->rx = state->freepos->vx - camera.pos.vx;
state->freepos->ry = state->freepos->vy - camera.pos.vy;
}
}
// A bubble can be of three diameters: small (8), medium (12) or big (32).
// Animation dictates object diameter.
int32_t diameter = 0;
switch(state->anim_state.animation) {
default:
case 0: diameter = 8; break; // small (breath)
case 1: diameter = 12; break; // medium
case 2: diameter = 32; break; // big
}
// The following movement logic should only work on common bubbles, and
// on number bubbles before the actual number frames show up
if(!((state->anim_state.animation >= 3) && (state->anim_state.frame >= 5))) {
// Bubbles should always be ascending with a 0.5 speed (-0x800)
if(state->anim_state.animation < 3)
state->freepos->vy -= 0x800;
else state->freepos->ry -= 0x800;
// Bubbles also sway back-and-forth in a sine-like movement.
// x = initial_x + 8 * sin(timer / 128.0)
state->timer--;
if(state->timer == 0) {
state->freepos->spdx *= -1;
state->timer = 128;
}
if(state->anim_state.animation < 3)
state->freepos->vx += state->freepos->spdx;
else state->freepos->rx += state->freepos->spdx;
}
// Again: if this is a stick-around bubble (number bubble), our free position
// should be relative to camera center
if(state->anim_state.animation >= 3) {
state->freepos->vx = state->freepos->rx + camera.pos.vx;
state->freepos->vy = state->freepos->ry + camera.pos.vy;
}
// When a normal bubble's top interact with water surface, destroy it.
// When a number bubble finishes its animation, destroy it.
if(((state->freepos->vy - (diameter << 12) <= level_water_y)
&& (state->anim_state.animation < 3))
|| (state->anim_state.animation == OBJ_ANIMATION_NO_ANIMATION)) {
state->props |= OBJ_FLAG_DESTROYED;
return;
}
// Bubbles can also only be interacted when big and on last animation frame.
// There is no destruction animation because of VRAM constraints; we still
// technically have a whole area available to add it, but I felt like I
// shouldn't create a whole new texture this time just because of a single
// bubble frame.
if((state->anim_state.animation == 2) && (state->anim_state.frame == 5)) {
// Bubble has an active trigger area of 32x16 at its bottom so we
// always overlap Sonic's mouth.
if(aabb_intersects(player_vx, player_vy, player_width, player_height,
pos->vx - 16, pos->vy - 16, 32, 16)) {
state->props |= OBJ_FLAG_DESTROYED;
player.remaining_air_frames = 1800;
// TODO: Cancel any drowning music.
// TODO: Setup proper action.
player.action = ACTION_NONE;
player_set_animation_direct(&player, ANIM_WALKING); // TODO!!!
player.grnd = 0;
player.vel.vx = player.vel.vy = player.vel.vz = 0;
sound_play_vag(sfx_bubble, 0);
return;
}
}
}