RingRacers/src/objects/gardentop.c
James R 8e9b9dd0aa Preserve scale of Garden Top when thrown
- If you deploy a Garden Top while grown/shrunk, the Top
  retains its size even after the grow/shrink effect runs
  out
- When the Top was thrown, however, it always used default
  scale
- Now the thrown Top keeps its scale intact
2024-04-02 05:00:21 -07:00

722 lines
13 KiB
C

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2024 by James Robert Roman.
// Copyright (C) 2024 by Kart Krew.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
#include "../doomdef.h"
#include "../doomstat.h"
#include "../info.h"
#include "../k_kart.h"
#include "../k_objects.h"
#include "../m_random.h"
#include "../p_local.h"
#include "../r_local.h"
#include "../s_sound.h"
// TODO: separate from this file
static fixed_t K_FlipZOffset(mobj_t *us, mobj_t *them)
{
fixed_t z = 0;
if (them->eflags & MFE_VERTICALFLIP)
z += them->height;
if (us->eflags & MFE_VERTICALFLIP)
z -= us->height;
return z;
}
#define SPARKCOLOR SKINCOLOR_ROBIN
enum {
TOP_ANCHORED,
TOP_LOOSE,
};
#define topsfx_floating sfx_s3k7d
#define topsfx_grinding sfx_s3k79
#define topsfx_lift sfx_s3ka0
#define rider_top(o) ((o)->hnext)
/* All Top states */
#define top_mode(o) ((o)->extravalue1)
#define top_float(o) ((o)->lastlook)
#define top_sound(o) ((o)->extravalue2)
#define top_soundtic(o) ((o)->movecount)
#define top_helpme(o) ((o)->cusval)
#define top_lifetime(o) ((o)->cvmem)
/* TOP_ANCHORED */
#define top_rider(o) ((o)->tracer)
/* TOP_LOOSE */
#define top_waveangle(o) ((o)->movedir)
/* wavepause will take mobjinfo reactiontime automatically */
#define top_wavepause(o) ((o)->reactiontime)
#define spark_top(o) ((o)->target)
#define spark_angle(o) ((o)->movedir)
enum {
ARROW_OVERHEAD,
ARROW_IN_FRONT,
};
#define arrow_top(o) ((o)->target)
static inline player_t *
get_rider_player (mobj_t *rider)
{
return rider ? rider->player : NULL;
}
static inline player_t *
get_top_rider_player (mobj_t *top)
{
return get_rider_player(top_rider(top));
}
static inline boolean
is_top_grind_input (mobj_t *top)
{
player_t *player = get_top_rider_player(top);
return player && K_IsHoldingDownTop(player);
}
static inline boolean
is_top_grinding (mobj_t *top)
{
if (top_float(top) > 0)
return false;
if (!P_IsObjectOnGround(top))
return false;
return true;
}
static inline fixed_t
grind_spark_base_scale (player_t *player)
{
return FRACUNIT/2 +
player->topdriftheld * FRACUNIT
/ GARDENTOP_MAXGRINDTIME;
}
static inline INT32
get_player_steer_tilt
( player_t * player,
INT32 stages)
{
return player->steering
* stages
// 1 degree for a full turn
/ KART_FULLTURN
* ANG1
// stages is for fractions of a full turn, divide to
// get a fraction of a degree
/ stages
// angle is inverted in reverse gravity
* P_MobjFlip(player->mo);
}
static inline fixed_t
goofy_shake (fixed_t n)
{
return P_RandomRange(PR_DECORATION, -1, 1) * n;
}
static inline void
init_top
( mobj_t * top,
INT32 mode)
{
top_mode(top) = mode;
top_float(top) = 0;
top_sound(top) = sfx_None;
top_waveangle(top) = 0;
top_helpme(top) = (mode == TOP_ANCHORED) ? 1 : 0;
top_lifetime(top) = 0;
}
static void
spawn_spark
( mobj_t * top,
angle_t angle)
{
mobj_t *spark = P_SpawnMobjFromMobj(
top, 0, 0, 0, MT_GARDENTOPSPARK);
P_SetTarget(&spark_top(spark), top);
spark_angle(spark) = angle;
spark->color = SPARKCOLOR;
spark->spriteyscale = 3*FRACUNIT/4;
}
static void
spawn_spark_circle
( mobj_t * top,
UINT8 n)
{
const angle_t a = ANGLE_MAX / n;
UINT8 i;
for (i = 0; i < n; ++i)
{
spawn_spark(top, i * a);
}
}
static void
spawn_grind_spark (mobj_t *top)
{
mobj_t *rider = top_rider(top);
mobj_t *spark;
player_t *player = NULL;
fixed_t x = 0;
fixed_t y = 0;
angle_t angle = top->angle;
if (rider)
{
const fixed_t speed = -20 * top->scale;
angle = K_MomentumAngle(rider);
x = P_ReturnThrustX(rider, angle, speed);
y = P_ReturnThrustY(rider, angle, speed);
player = get_rider_player(rider);
}
top_helpme(top) = 0;
spark = P_SpawnMobjFromMobj(
top, x, y, 0, MT_DRIFTSPARK);
spark->momx = x;
spark->momy = y;
P_SetMobjState(spark, S_DRIFTSPARK_A1);
spark->angle = angle;
spark->color = SPARKCOLOR;
if (player)
{
spark->destscale = FixedMul(spark->destscale,
grind_spark_base_scale(player));
P_SetScale(spark, spark->destscale);
}
}
static void
spawn_arrow (mobj_t *top)
{
mobj_t *arrow = P_SpawnMobjFromMobj(
top, 0, 0, 0, MT_GARDENTOPARROW);
P_SetTarget(&arrow_top(arrow), top);
P_SetScale(arrow,
(arrow->destscale = 3 * arrow->scale / 4));
}
static void
loop_sfx
( mobj_t * top,
sfxenum_t sfx)
{
switch (sfx)
{
case topsfx_floating:
if (S_SoundPlaying(top, sfx))
{
return;
}
break;
case topsfx_grinding:
if ((sfxenum_t)top_sound(top) != sfx)
{
top_soundtic(top) = leveltime;
}
/* FIXME: could this sound just be looped
normally? :face_holding_back_tears: */
if ((leveltime - top_soundtic(top)) % 28 > 0)
{
return;
}
break;
default:
break;
}
S_StartSound(top, sfx);
}
static void
modulate (mobj_t *top)
{
const fixed_t max_hover = top->height / 4;
const fixed_t hover_step = max_hover / 4;
sfxenum_t ambience = sfx_None;
if (is_top_grind_input(top))
{
if (top_float(top) == max_hover)
{
P_SetMobjState(top, S_GARDENTOP_SINKING1);
}
if (top_float(top) > 0)
{
top_float(top) = max(0,
top_float(top) - hover_step);
}
else if (P_IsObjectOnGround(top))
{
spawn_grind_spark(top);
ambience = topsfx_grinding;
}
}
else
{
if (top_float(top) == 0)
{
P_SetMobjState(top, S_GARDENTOP_FLOATING);
S_StopSoundByID(top, topsfx_grinding);
S_StartSound(top, topsfx_lift);
}
if (top_float(top) < max_hover)
{
top_float(top) = min(max_hover,
top_float(top) + hover_step);
}
else
{
ambience = topsfx_floating;
}
}
top->sprzoff = top_float(top) * P_MobjFlip(top);
if (ambience)
{
loop_sfx(top, ambience);
}
top_sound(top) = ambience;
}
static void
tilt (mobj_t *top)
{
player_t *player = get_top_rider_player(top);
INT32 tilt = top->rollangle;
if (is_top_grind_input(top))
{
const angle_t tiltmax = ANGLE_22h;
tilt += get_player_steer_tilt(player, 4);
if (abs(tilt) > tiltmax)
{
tilt = intsign(tilt) * tiltmax;
}
}
else
{
const angle_t decay = ANG1 * 2;
if (abs(tilt) > decay)
{
tilt -= intsign(tilt) * decay;
}
else
{
tilt = 0;
}
}
top->rollangle = tilt;
/* Vibrate left and right if you're about to lose it. */
if (player && player->topinfirst)
{
top->spritexoffset = P_LerpFlip(32*FRACUNIT, 1);
}
else
{
top->spritexoffset = 0;
}
/* Go ABSOLUTELY NUTS if the player is tumbling... */
if (player && player->tumbleBounces > 0)
{
const fixed_t yofs = 48 * FRACUNIT;
const fixed_t ofs3d = 24 * top->scale;
/* spriteyoffset scales, e.g. with K_Squish */
top->spriteyoffset = FixedDiv(
goofy_shake(yofs), top->spriteyscale);
top->sprxoff = goofy_shake(ofs3d);
top->spryoff = goofy_shake(ofs3d);
}
else
{
top->spriteyoffset = 0;
top->sprxoff = 0;
top->spryoff = 0;
}
}
static void
anchor
( mobj_t * us,
mobj_t * them,
angle_t angle,
fixed_t radius)
{
const fixed_t x = P_ReturnThrustX(us, angle, radius);
const fixed_t y = P_ReturnThrustY(us, angle, radius);
/* FIXME: THIS FUNCTION FUCKING SUCKS */
K_FlipFromObject(us, them);
P_MoveOrigin(us, them->x + x, them->y + y,
them->z + K_FlipZOffset(us, them));
us->angle = angle;
}
static void
anchor_top (mobj_t *top)
{
mobj_t *rider = top_rider(top);
player_t *player = get_rider_player(rider);
if (player && player->curshield != KSHIELD_TOP)
{
P_RemoveMobj(top);
return;
}
/* Rider lost track of this object. */
if (rider_top(rider) != top)
{
P_RemoveMobj(top);
return;
}
tilt(top);
anchor(top, rider, rider->angle, 0);
K_GenericExtraFlagsNoZAdjust(top, rider);
/* Copying the Z momentum lets the Top squash and stretch
as it falls with the player. Don't copy the X/Y
momentum because then it would always get slightly
ahead of the player. */
top->momx = 0;
top->momy = 0;
top->momz = rider->momz;
top_lifetime(top)++;
/* The Z momentum can put the Top slightly ahead of the
player in that axis too. It looks cool if the Top
falls below you but not if it bounces up. */
if (top->momz * P_MobjFlip(top) > 0)
{
top->momz = 0;
}
/* match rider's slope tilt */
top->pitch = rider->pitch;
top->roll = rider->roll;
}
static void
loose_think (mobj_t *top)
{
const fixed_t thrustamount = top->movefactor / 2;
const angle_t momangle = K_MomentumAngle(top);
angle_t ang = top->angle;
mobj_t *ghost = P_SpawnGhostMobj(top);
ghost->colorized = true; // already has color!
if (AngleDelta(ang, momangle) > ANGLE_90)
{
top->angle = momangle;
}
if (top_wavepause(top))
{
top_wavepause(top)--;
}
else
{
/* oscillate between +90 and -90 degrees */
ang += AbsAngle(top_waveangle(top)) - ANGLE_90;
}
P_InstaThrust(top, top->angle, thrustamount);
P_Thrust(top, ang, thrustamount);
//top_waveangle(top) = (angle_t)top_waveangle(top) + ANG10;
top_waveangle(top) += ANG10;
/* intangibility grace period */
if (top->threshold > 0)
{
top->threshold--;
}
}
static void
anchor_spark (mobj_t *spark)
{
mobj_t *top = spark_top(spark);
mobj_t *rider = top_rider(top);
player_t *player = get_rider_player(rider);
anchor(spark, top,
(top->angle + spark_angle(spark)), spark->scale);
if (player)
{
const fixed_t topspeed =
K_GetKartSpeed(player, false, false);
const fixed_t speed = FixedHypot(
rider->momx, rider->momy);
P_SetScale(spark, FixedMul(top->scale, FRACUNIT/2 +
FixedDiv(speed / 2, topspeed)));
}
}
static void
anchor_arrow_overhead (mobj_t *arrow)
{
mobj_t *top = arrow_top(arrow);
mobj_t *rider = top_rider(top);
const fixed_t height =
top->height + rider->height + (3 * arrow->height / 4);
arrow->sprzoff = top->sprzoff +
(height * P_MobjFlip(arrow));
anchor(arrow, top, rider->angle + ANGLE_180, 0);
}
void
Obj_GardenTopDeploy (mobj_t *rider)
{
player_t *player = rider->player;
mobj_t *top = P_SpawnMobjFromMobj(
rider, 0, 0, 0, MT_GARDENTOP);
init_top(top, TOP_ANCHORED);
top->flags |= MF_NOCLIPHEIGHT;
/* only the player's shadow needs to be rendered */
top->shadowscale = 0;
P_SetTarget(&top_rider(top), rider);
P_SetTarget(&rider_top(rider), top);
if (player)
{
player->curshield = KSHIELD_TOP;
rider->radius = K_DefaultPlayerRadius(player);
/* Doing this here to set itemscale.
And unset right afterward so the item box doesn't flicker! */
K_SetItemOut(player);
P_InstaScale(top, K_ItemScaleForPlayer(player));
K_UnsetItemOut(player);
}
spawn_spark_circle(top, 6);
spawn_arrow(top);
}
mobj_t *
Obj_GardenTopThrow (player_t *player)
{
mobj_t *top = K_GetGardenTop(player);
if (top)
{
const fixed_t oldfloat = top_float(top);
const fixed_t height = top->height;
/* Sucks that another one needs to be spawned but
this way, the throwing function can be used. */
top = K_ThrowKartItem(
player, true, MT_GARDENTOP, 1, 0, 0);
K_UpdateHnextList(player, true);
init_top(top, TOP_LOOSE);
top_float(top) = oldfloat;
top_waveangle(top) = 0;
/* prevents it from hitting us on its way out */
top->threshold = 20;
/* ensure it's tangible */
top->flags &= ~(MF_NOCLIPTHING);
/* Put player PHYSICALLY on top. While riding the
Top, player collision was used and the player
technically remained on the ground. Now they
should fall off. */
P_SetOrigin(player->mo, player->mo->x, player->mo->y,
player->mo->z + height * P_MobjFlip(player->mo));
}
if (player->itemamount > 0)
player->itemamount--;
if (player->itemamount <= 0)
player->itemtype = KITEM_NONE;
player->curshield = KSHIELD_NONE;
player->mo->radius = K_DefaultPlayerRadius(player);
return top;
}
mobj_t *
Obj_GardenTopDestroy (player_t *player)
{
mobj_t *top = Obj_GardenTopThrow(player);
if (top)
{
/* kill kill kill die die die */
P_KillMobj(top, NULL, NULL, DMG_NORMAL);
}
return top;
}
void
Obj_GardenTopThink (mobj_t *top)
{
modulate(top);
switch (top_mode(top))
{
case TOP_ANCHORED:
if (top_rider(top))
{
anchor_top(top);
}
break;
case TOP_LOOSE:
loose_think(top);
break;
}
}
void
Obj_GardenTopSparkThink (mobj_t *spark)
{
mobj_t *top = spark_top(spark);
if (!top)
{
P_RemoveMobj(spark);
return;
}
anchor_spark(spark);
if (is_top_grinding(top))
{
spark->renderflags ^= RF_DONTDRAW;
}
else
{
spark->renderflags |= RF_DONTDRAW;
}
}
void
Obj_GardenTopArrowThink (mobj_t *arrow)
{
mobj_t *top = arrow_top(arrow);
mobj_t *rider = top ? top_rider(top) : NULL;
if (!rider)
{
P_RemoveMobj(arrow);
return;
}
anchor_arrow_overhead(arrow);
if (rider->player)
{
// Don't show for other players
arrow->renderflags =
(arrow->renderflags & ~(RF_DONTDRAW)) |
(RF_DONTDRAW & ~(K_GetPlayerDontDrawFlag(rider->player)));
}
}
boolean
Obj_GardenTopPlayerIsGrinding (const player_t *player)
{
mobj_t *top = K_GetGardenTop(player);
return top ? is_top_grinding(top) : false;
}
boolean
Obj_GardenTopPlayerNeedsHelp (const mobj_t *top)
{
if (top && top_mode(top) != TOP_ANCHORED)
return false;
return top ? (top_helpme(top) || top_lifetime(top) < 3*TICRATE) : false;
}