Merge branch 'ancient-gear' into 'master'

Ancient Gear (resolves #1599)

Closes #1599

See merge request kart-krew-dev/ring-racers-internal!2726
This commit is contained in:
Oni VelocitOni 2025-08-10 00:43:23 +00:00
commit c7b3a49c85
20 changed files with 398 additions and 8 deletions

View file

@ -3294,7 +3294,8 @@ static void readcondition(UINT16 set, UINT32 id, char *word2)
|| (++offset && fastcmp(params[0], "HITMIDAIR"))
|| (++offset && fastcmp(params[0], "HITDRAFTERLOOKBACK"))
|| (++offset && fastcmp(params[0], "GIANTRACERSHRUNKENORBI"))
|| (++offset && fastcmp(params[0], "RETURNMARKTOSENDER")))
|| (++offset && fastcmp(params[0], "RETURNMARKTOSENDER"))
|| (++offset && fastcmp(params[0], "ALLANCIENTGEARS")))
{
//PARAMCHECK(1);
ty = UCRP_TRIPWIREHYUU + offset;

View file

@ -3120,6 +3120,9 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
"S_TOXAA_DEAD",
"S_TOXAB",
"S_TOXBA",
"S_ANCIENTGEAR",
"S_ANCIENTGEAR_PART",
};
// RegEx to generate this from info.h: ^\tMT_([^,]+), --> \t"MT_\1",
@ -4036,6 +4039,9 @@ const char *const MOBJTYPE_LIST[] = { // array length left dynamic for sanity t
"MT_TOXOMISTER_POLE",
"MT_TOXOMISTER_EYE",
"MT_TOXOMISTER_CLOUD",
"MT_ANCIENTGEAR",
"MT_ANCIENTGEAR_PART",
};
const char *const MOBJFLAG_LIST[] = {

View file

@ -812,6 +812,8 @@ char sprnames[NUMSPRITES + 1][5] =
"TOXA",
"TOXB",
"GEAR",
// Pulley
"HCCH",
"HCHK",
@ -3713,6 +3715,9 @@ state_t states[NUMSTATES] =
{SPR_TOXA, 0, 175, {NULL}, 0, 0, S_NULL}, // S_TOXAA_DEAD
{SPR_TOXA, 1, -1, {NULL}, 0, 0, S_TOXAB}, // S_TOXAB
{SPR_TOXB, FF_ANIMATE|FF_RANDOMANIM, -1, {NULL}, 6, 5, S_TOXBA}, // S_TOXBA
{SPR_GEAR, 0, -1, {NULL}, 0, 0, S_NULL}, // S_ANCIENTGEAR
{SPR_GEAR, FF_PAPERSPRITE|1, -1, {NULL}, 0, 0, S_NULL}, // S_ANCIENTGEAR_PART
};
mobjinfo_t mobjinfo[NUMMOBJTYPES] =
@ -22880,6 +22885,58 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
MF_SPECIAL|MF_NOGRAVITY|MF_DONTENCOREMAP|MF_ELEMENTAL, // flags
S_NULL // raisestate
},
{ // MT_ANCIENTGEAR
323, // doomednum
S_ANCIENTGEAR, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_gotgea, // seesound
8, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_ANCIENTGEAR, // deathstate
S_NULL, // xdeathstate
sfx_gshc4, // deathsound
0, // speed
64*FRACUNIT, // radius
128*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_SPECIAL|MF_NOGRAVITY|MF_DONTENCOREMAP|MF_NOSQUISH, // flags
S_NULL // raisestate
},
{ // MT_ANCIENTGEAR_PART
-1, // doomednum
S_ANCIENTGEAR_PART, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound
8, // reactiontime
sfx_None, // attacksound
S_NULL, // painstate
0, // painchance
sfx_None, // painsound
S_NULL, // meleestate
S_NULL, // missilestate
S_NULL, // deathstate
S_NULL, // xdeathstate
sfx_None, // deathsound
0, // speed
64*FRACUNIT, // radius
32*FRACUNIT, // height
0, // display offset
0, // mass
0, // damage
sfx_None, // activesound
MF_SCENERY|MF_NOGRAVITY|MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPTHING|MF_NOCLIPHEIGHT|MF_DONTENCOREMAP|MF_NOSQUISH, // flags
S_NULL // raisestate
},
};

View file

@ -1349,6 +1349,8 @@ typedef enum sprite
SPR_TOXA,
SPR_TOXB,
SPR_GEAR,
// Pulley
SPR_HCCH,
SPR_HCHK,
@ -4197,6 +4199,9 @@ typedef enum state
S_TOXAB,
S_TOXBA,
S_ANCIENTGEAR,
S_ANCIENTGEAR_PART,
S_FIRSTFREESLOT,
S_LASTFREESLOT = S_FIRSTFREESLOT + NUMSTATEFREESLOTS - 1,
NUMSTATES
@ -5136,6 +5141,9 @@ typedef enum mobj_type
MT_TOXOMISTER_EYE,
MT_TOXOMISTER_CLOUD,
MT_ANCIENTGEAR,
MT_ANCIENTGEAR_PART,
MT_FIRSTFREESLOT,
MT_LASTFREESLOT = MT_FIRSTFREESLOT + NUMMOBJFREESLOTS - 1,
NUMMOBJTYPES

View file

@ -233,7 +233,8 @@ void K_CheckEmeralds(player_t *player)
player->angleturn + ANGLE_180,
400*mapobjectscale,
6*TICRATE,
FRACUNIT/16
FRACUNIT/16,
3*TICRATE
);
g_emeraldWin += g_endcam.swirlDuration;

View file

@ -131,6 +131,11 @@ struct EndCam : endcam_t
}
void Stop()
{
active = false;
}
template <typename T>
void Archive(T&& ar)
{
@ -203,7 +208,7 @@ void K_LoadEndCamera(savebuffer_t *save)
endcam_cast().Archive(srb2::UnArchiveWrapper(save));
}
void K_StartRoundWinCamera(mobj_t *origin, angle_t focusAngle, fixed_t finalRadius, tic_t panDuration, fixed_t panSpeed)
void K_StartRoundWinCamera(mobj_t *origin, angle_t focusAngle, fixed_t finalRadius, tic_t panDuration, fixed_t panSpeed, tic_t swirlDuration)
{
const fixed_t angF = AngleFixed(focusAngle);
@ -211,7 +216,7 @@ void K_StartRoundWinCamera(mobj_t *origin, angle_t focusAngle, fixed_t finalRadi
g_endcam.startRadius = {2400*mapobjectscale, 800*mapobjectscale};
g_endcam.endRadius = {finalRadius, finalRadius / 2};
g_endcam.swirlDuration = 3*TICRATE;
g_endcam.swirlDuration = swirlDuration;
g_endcam.startAngle = angF + (90*FRACUNIT);
g_endcam.endAngle = angF + (720*FRACUNIT);
@ -224,3 +229,8 @@ void K_StartRoundWinCamera(mobj_t *origin, angle_t focusAngle, fixed_t finalRadi
g_darkness.start = leveltime;
g_darkness.end = leveltime + g_endcam.swirlDuration + DARKNESS_FADE_TIME;
}
void K_StopRoundWinCamera(void)
{
endcam_cast().Stop();
}

View file

@ -60,7 +60,10 @@ extern endcam_t g_endcam;
void K_CommitEndCamera(void);
// Automatically set up a cool camera in one-shot.
void K_StartRoundWinCamera(mobj_t *origin, angle_t focusAngle, fixed_t finalRadius, tic_t panDuration, fixed_t panSpeed);
void K_StartRoundWinCamera(mobj_t *origin, angle_t focusAngle, fixed_t finalRadius, tic_t panDuration, fixed_t panSpeed, tic_t swirlDuration);
// Stop the end camera
void K_StopRoundWinCamera(void);
/// ...

View file

@ -8038,6 +8038,17 @@ void K_drawKartHUD(void)
}
}
if (stplyr == Obj_GetAncientGearCollectingPlayer())
{
srb2::Draw(BASEVIDWIDTH / 2, 130)
.flags(V_SNAPTOBOTTOM)
.font(srb2::Draw::Font::kGamemode)
.align(srb2::Draw::Align::kCenter)
.scale(0.80f)
.text("YOU GOT AN ANCIENT GEAR")
;
}
// TODO better voice chat speaking indicator integration for spectators
{
char speakingstring[2048];

View file

@ -4589,7 +4589,8 @@ void K_CheckpointCrossAward(player_t *player)
player->angleturn + ANGLE_180,
400*mapobjectscale,
6*TICRATE,
FRACUNIT/16
FRACUNIT/16,
3*TICRATE
);
}
@ -4835,7 +4836,8 @@ void K_BattleAwardHit(player_t *player, player_t *victim, mobj_t *inflictor, UIN
R_PointToAngle2(source->x, source->y, victim->mo->x, victim->mo->y) + ANGLE_135,
200*mapobjectscale,
8*TICRATE,
FRACUNIT/512
FRACUNIT/512,
3*TICRATE
);
}

View file

@ -488,6 +488,17 @@ boolean Obj_ToxomisterPoleCollide(mobj_t *pole, mobj_t *toucher);
boolean Obj_ToxomisterCloudCollide(mobj_t *cloud, mobj_t *toucher);
fixed_t Obj_GetToxomisterCloudDrag(mobj_t *cloud);
/* Ancient Gear */
void Obj_AncientGearSpawn(mobj_t *gear);
void Obj_AncientGearPartThink(mobj_t *part);
void Obj_AncientGearRemoved(mobj_t *gear);
void Obj_AncientGearTouch(mobj_t *gear, mobj_t *toucher);
void Obj_AncientGearDeath(mobj_t *gear, mobj_t *source);
void Obj_AncientGearDeadThink(mobj_t *gear);
void Obj_AncientGearLevelInit(void);
player_t *Obj_GetAncientGearCollectingPlayer(void);
boolean Obj_AllAncientGearsCollected(void);
#ifdef __cplusplus
} // extern "C"

View file

@ -668,7 +668,9 @@ static mapthing_t *OP_CreateNewMapThing(player_t *player, UINT16 type, boolean c
mt->angle = (INT16)(FixedInt(AngleFixed(player->mo->angle)));
mt->options = (mt->z << ZSHIFT) | (UINT16)cv_opflags.value;
mt->scale = player->mo->scale;
mt->scale = FixedDiv(player->mo->scale, mapobjectscale);
mt->spritexscale = FRACUNIT;
mt->spriteyscale = FRACUNIT;
memset(mt->thing_args, 0, NUM_MAPTHING_ARGS*sizeof(*mt->thing_args));
memset(mt->thing_stringargs, 0x00, NUM_MAPTHING_STRINGARGS*sizeof(*mt->thing_stringargs));
mt->special = 0;

View file

@ -33,6 +33,7 @@
#include "k_podium.h"
#include "k_pwrlv.h"
#include "k_profiles.h"
#include "k_objects.h" // Obj_AllAncientGearsCollected
gamedata_t *gamedata = NULL;
boolean netUnlocked[MAXUNLOCKABLES];
@ -2035,6 +2036,8 @@ boolean M_CheckCondition(condition_t *cn, player_t *player)
return (player->roundconditions.giant_foe_shrunken_orbi);
case UCRP_RETURNMARKTOSENDER:
return (player->roundconditions.returntosender_mark);
case UCRP_ALLANCIENTGEARS:
return Obj_AllAncientGearsCollected();
case UCRP_TRACKHAZARD:
{
@ -2948,6 +2951,8 @@ static const char *M_GetConditionString(condition_t *cn)
return "hit a giant racer with a shrunken Orbinaut";
case UCRP_RETURNMARKTOSENDER:
return "when cursed with Eggmark, blow up the racer responsible";
case UCRP_ALLANCIENTGEARS:
return "collect all Ancient Gears";
case UCRP_TRACKHAZARD:
{

View file

@ -148,6 +148,7 @@ typedef enum
UCRP_HITDRAFTERLOOKBACK, // Hit a player that's behind you, while looking back at them, and they're drafting off you
UCRP_GIANTRACERSHRUNKENORBI, // Hit a giant racer with a shrunken Orbinaut
UCRP_RETURNMARKTOSENDER, // Hit the player responsible for Eggman Marking you with that explosion
UCRP_ALLANCIENTGEARS, // Collect all Ancient Gears in a map
UCRP_TRACKHAZARD, // (Don't) get hit by a track hazard (maybe specific lap)

View file

@ -68,6 +68,7 @@ target_sources(SRB2SDL2 PRIVATE
exp.c
bail.c
toxomister.cpp
ancient-gear.c
)
add_subdirectory(versus)

237
src/objects/ancient-gear.c Normal file
View file

@ -0,0 +1,237 @@
// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2025 by Lachlan "Lach" Wright
// Copyright (C) 2025 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.
//-----------------------------------------------------------------------------
/// \file ancient-gear.c
/// \brief Ancient Gear object code.
#include "../p_local.h"
#include "../k_objects.h"
#include "../s_sound.h"
#include "../k_endcam.h"
#include "../k_hud.h"
#include "../m_cond.h"
#define DEATH_FREEZE_TIME (5*TICRATE)
#define DEATH_TIME (2*TICRATE)
#define DEATH_RISE_SPEED (3*FRACUNIT)
#define DELTA_YAW (-ANG2)
#define DELTA_ROLL (ANG2)
static const angle_t DELTA_YAW_ACCELERATION = (-ANG1 / 2);
static const angle_t DELTA_ROLL_ACCELERATION = (ANG1 / 3);
static const fixed_t DELTA_SPRITEXSCALE = ((FRACUNIT/4 - FRACUNIT) / DEATH_TIME);
static const fixed_t DELTA_SPRITEYSCALE = ((FRACUNIT*3 - FRACUNIT) / DEATH_TIME);
static const tic_t TRANS_SHIFT_RATE = (DEATH_TIME / 10);
static UINT16 numGears = 0;
static boolean allGearsCollected = false;
static player_t *collectingPlayer = NULL;
static void UpdateAncientGearPart(mobj_t *part)
{
mobj_t *gear = part->target;
fixed_t radius = FixedMul(FixedMul(part->movefactor, gear->scale), gear->spritexscale);
fixed_t xOffset = P_ReturnThrustX(NULL, part->angle - ANGLE_90, radius);
fixed_t yOffset = P_ReturnThrustY(NULL, part->angle - ANGLE_90, radius);
P_InstaScale(part, gear->scale);
P_MoveOrigin(part,
gear->x + xOffset,
gear->y + yOffset,
gear->z + gear->height / 2
);
part->angle += gear->extravalue1;
part->rollangle += gear->extravalue2 * part->extravalue1;
part->spritexscale = gear->spritexscale;
part->spriteyscale = gear->spriteyscale;
}
void Obj_AncientGearSpawn(mobj_t *gear)
{
UINT8 i;
mobj_t *part = gear;
numGears++;
gear->extravalue1 = DELTA_YAW;
gear->extravalue2 = DELTA_ROLL;
for (i = 0; i < 6; i++)
{
P_SetTarget(&part->hnext, P_SpawnMobjFromMobj(gear, 0, 0, 0, MT_ANCIENTGEAR_PART));
P_SetTarget(&part->hnext->hprev, part);
part = part->hnext;
P_SetTarget(&part->target, gear);
part->angle += (i & 1) * ANGLE_180;
if (i < 2) // middle parts
{
part->angle += ANGLE_90;
part->movefactor = 10 * FRACUNIT; // horizontal offset from hitbox
part->extravalue1 = 0; // direction to roll the sprite
part->frame = (part->frame & ~FF_FRAMEMASK) | 1;
P_SetTarget(&part->tracer, gear);
part->flags2 |= MF2_LINKDRAW;
}
else // side parts
{
part->movefactor = 7 * FRACUNIT; // horizontal offset from hitbox
part->extravalue1 = (i & 1) * 2 - 1; // direction to roll the sprite
if (i > 3) // fake brightmaps
{
part->frame = (part->frame & ~FF_FRAMEMASK) | 3 | FF_FULLBRIGHT;
part->dispoffset++;
}
else
{
part->frame = (part->frame & ~FF_FRAMEMASK) | 2;
}
}
UpdateAncientGearPart(part);
part->old_x = part->x;
part->old_y = part->y;
part->old_z = part->z;
part->old_angle = part->angle;
part->old_scale = part->scale;
}
}
void Obj_AncientGearPartThink(mobj_t *part)
{
if (P_MobjWasRemoved(part->target))
{
P_RemoveMobj(part);
return;
}
UpdateAncientGearPart(part);
}
void Obj_AncientGearRemoved(mobj_t *gear)
{
while (!P_MobjWasRemoved(gear->hnext))
{
P_RemoveMobj(gear->hnext);
}
}
void Obj_AncientGearTouch(mobj_t *gear, mobj_t *toucher)
{
P_KillMobj(gear, NULL, toucher, DMG_NORMAL);
}
void Obj_AncientGearDeath(mobj_t *gear, mobj_t *source)
{
if (--numGears == 0)
{
allGearsCollected = true;
M_UpdateUnlockablesAndExtraEmblems(true, true);
}
gear->fuse = DEATH_TIME;
gear->shadowscale = gear->spritexscale;
// give the gear some upwards momentum
gear->flags |= MF_NOCLIPHEIGHT;
P_SetObjectMomZ(gear, DEATH_RISE_SPEED, false);
// don't activate the round win camera if there is no camera target,
// or if the round win camera is already active for any actual reason
if (P_MobjWasRemoved(source) || source->player == NULL || g_endcam.active)
{
return;
}
// track the collecting player to display a message for them
collectingPlayer = source->player;
P_SetTarget(&gear->target, source);
// play the collection jingle!
S_StartSound(NULL, gear->info->seesound);
// fade out the music for as long as the sound plays
g_musicfade.start = leveltime;
g_musicfade.end = g_musicfade.start + DEATH_FREEZE_TIME;
g_musicfade.fade = 12;
g_musicfade.ticked = false;
// start the round win camera
gear->flags2 |= MF2_BEYONDTHEGRAVE; // a gear with this flag will stop the round win camera upon next thinking
K_StartRoundWinCamera(
source,
source->player->angleturn,
FixedMul(cv_cam_dist[0].value, mapobjectscale),
6*TICRATE,
FRACUNIT/16,
DEATH_FREEZE_TIME
);
}
void Obj_AncientGearDeadThink(mobj_t *gear)
{
mobj_t *part = gear;
// if the round win camera was activated, tell it to stop focusing on the player,
// and show the player a message
if (gear->flags2 & MF2_BEYONDTHEGRAVE)
{
gear->flags2 &= ~MF2_BEYONDTHEGRAVE;
K_StopRoundWinCamera();
collectingPlayer = NULL;
K_AddMessage(
numGears > 0 ? va("%d Ancient Gear%s left!", numGears, numGears == 1 ? "" : "s")
: "All Ancient Gears collected!"
, true, false
);
}
// play another sound once immediately after the round win camera finishes
if (!(gear->flags2 & MF2_DONTRESPAWN))
{
gear->flags2 |= MF2_DONTRESPAWN;
S_StartSound(gear, gear->info->deathsound);
}
// increase the translucency level every so often
if (gear->fuse % TRANS_SHIFT_RATE == 0)
{
while (!P_MobjWasRemoved(part = part->hnext))
{
part->frame += FF_TRANS10;
}
}
// accelerate the spinning and rotating
gear->extravalue1 += DELTA_YAW_ACCELERATION;
gear->extravalue2 += DELTA_ROLL_ACCELERATION;
// stretch the gear
gear->spritexscale += DELTA_SPRITEXSCALE;
gear->spriteyscale += DELTA_SPRITEYSCALE;
gear->shadowscale = gear->spritexscale;
}
void Obj_AncientGearLevelInit(void)
{
numGears = 0;
allGearsCollected = false;
collectingPlayer = NULL;
}
player_t *Obj_GetAncientGearCollectingPlayer(void)
{
return collectingPlayer;
}
boolean Obj_AllAncientGearsCollected(void)
{
return allGearsCollected;
}

View file

@ -1137,6 +1137,10 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
Obj_ToxomisterCloudCollide(special, toucher);
return;
case MT_ANCIENTGEAR:
Obj_AncientGearTouch(special, toucher);
return;
default: // SOC or script pickup
P_SetTarget(&special->target, toucher);
break;
@ -2364,6 +2368,9 @@ void P_KillMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, UINT8 damaget
case MT_FLYBOT767:
Obj_FlybotDeath(target);
break;
case MT_ANCIENTGEAR:
Obj_AncientGearDeath(target, source);
break;
default:
break;
}

View file

@ -6798,6 +6798,11 @@ static void P_MobjSceneryThink(mobj_t *mobj)
Obj_TickStoneShoeChain(mobj);
return;
}
case MT_ANCIENTGEAR_PART:
{
Obj_AncientGearPartThink(mobj);
return;
}
default:
if (mobj->fuse)
{ // Scenery object fuse! Very basic!
@ -7000,6 +7005,11 @@ static boolean P_MobjDeadThink(mobj_t *mobj)
VS_BlendEye_Generator_DeadThinker(mobj);
break;
}
case MT_ANCIENTGEAR:
{
Obj_AncientGearDeadThink(mobj);
break;
}
default:
break;
}
@ -11179,6 +11189,7 @@ static void P_DefaultMobjShadowScale(mobj_t *thing)
case MT_BATTLEUFO:
case MT_SPRAYCAN:
case MT_CHECKPOINT_END:
case MT_ANCIENTGEAR:
thing->shadowscale = FRACUNIT;
break;
case MT_SMALLMACE:
@ -11724,6 +11735,9 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
case MT_CABOTRON:
Obj_SSCabotronMobjSpawn(mobj);
break;
case MT_ANCIENTGEAR:
Obj_AncientGearSpawn(mobj);
break;
default:
break;
}
@ -12011,6 +12025,11 @@ void P_RemoveMobj(mobj_t *mobj)
Obj_FlybotRemoved(mobj);
break;
}
case MT_ANCIENTGEAR:
{
Obj_AncientGearRemoved(mobj);
break;
}
default:
{
break;

View file

@ -7634,6 +7634,8 @@ static void P_InitLevelSettings(void)
nummapspraycans = 0;
numchallengedestructibles = 0;
Obj_AncientGearLevelInit();
// circuit, race and competition stuff
numcheatchecks = 0;

View file

@ -1543,6 +1543,9 @@ sfxinfo_t S_sfx[NUMSFX] =
// :apple:
{"aple", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR, ""},
// Ancient Gear
{"gotgea", false, 64, 0, -1, NULL, 0, -1, -1, LUMPERROR, ""},
// SRB2kart - Skin sounds
{"kwin", false, 64, 96, -1, NULL, 0, SKSKWIN, -1, LUMPERROR, ""},
{"klose", false, 64, 96, -1, NULL, 0, SKSKLOSE, -1, LUMPERROR, ""},

View file

@ -1619,6 +1619,9 @@ typedef enum
// :apple:
sfx_aple,
// Ancient Gear
sfx_gotgea,
// And LASTLY, Kart's skin sounds.
sfx_kwin,
sfx_klose,