Merge pull request 'Follower horns' (#214) from universally-hated-in-dev into next

Reviewed-on: https://codeberg.org/NepDisk/blankart/pulls/214
This commit is contained in:
yamamama 2026-02-01 22:20:34 +01:00
commit 16d2789a15
19 changed files with 342 additions and 3 deletions

View file

@ -437,6 +437,7 @@ static CV_PossibleValue_t kartspeedometer_cons_t[] = {{0, "Off"}, {1, "Kilometer
consvar_t cv_kartspeedometer = CVAR_INIT ("kartdisplayspeed", "Percentage", CV_SAVE, kartspeedometer_cons_t, NULL); // use tics in display
static CV_PossibleValue_t kartvoices_cons_t[] = {{0, "Never"}, {1, "Tasteful"}, {2, "Meme"}, {0, NULL}};
consvar_t cv_kartvoices = CVAR_INIT ("kartvoices", "Tasteful", CV_SAVE, kartvoices_cons_t, NULL);
consvar_t cv_karthorns = CVAR_INIT ("taunthorns", "Tasteful", CV_SAVE, kartvoices_cons_t, NULL);
static CV_PossibleValue_t kartbot_cons_t[] = {
{0, "Off"},

View file

@ -88,6 +88,7 @@ extern consvar_t cv_kartvoterulechanges;
extern consvar_t cv_kartgametypepreference;
extern consvar_t cv_kartspeedometer;
extern consvar_t cv_kartvoices;
extern consvar_t cv_karthorns;
extern consvar_t cv_kartbot;
extern consvar_t cv_kartbot_cap;
extern consvar_t cv_kartbot_modifiermax;

View file

@ -206,6 +206,7 @@ typedef enum
khud_tauntvoices, // Used to specifically stop taunt voice spam
khud_confirmvictim, // Player ID that you dealt damage to
khud_confirmvictimdelay, // Delay before playing the sound
khud_taunthorns, // Used to specifically stop taunt horn spam
// Battle
khud_cardanimation, // Used to determine the position of some full-screen Battle Mode graphics

View file

@ -33,8 +33,9 @@ typedef enum
BT_FORWARD = 1<<5, // Aim Item Forward
BT_BACKWARD = 1<<6, // Aim Item Backward
BT_LOOKBACK = 1<<7, // Look Backward
BT_HORN = 1<<8, // Honk your (follower's) horn
// free: 1<<8 to 1<<12
// free: 1<<9 to 1<<12
// Lua garbage
BT_CUSTOM1 = 1<<13,

View file

@ -3762,7 +3762,9 @@ void readfollower(MYFILE *f)
followers[numfollowers].bobspeed = TICRATE*2;
followers[numfollowers].bobamp = 4*FRACUNIT;
followers[numfollowers].hitconfirmtime = TICRATE;
followers[numfollowers].horntime = TICRATE;
followers[numfollowers].defaultcolor = FOLLOWERCOLOR_MATCH;
followers[numfollowers].hornsound = sfx_horn00;
strcpy(followers[numfollowers].icon, "MISSING");
do
@ -3829,6 +3831,10 @@ void readfollower(MYFILE *f)
followers[numfollowers].defaultcolor = color == MAXSKINCOLORS ? SKINCOLOR_GREEN : color;
}
}
else if (fastcmp(word, "HORNSOUND"))
{
followers[numfollowers].hornsound = get_number(word2);
}
else if (fastcmp(word, "SCALE"))
{
followers[numfollowers].scale = (fixed_t)get_number(word2);
@ -3914,6 +3920,16 @@ void readfollower(MYFILE *f)
{
followers[numfollowers].hitconfirmtime = (tic_t)get_number(word2);
}
else if (fastcmp(word, "HORNSTATE"))
{
if (word2)
strupr(word2);
followers[numfollowers].hornstate = get_number(word2);
}
else if (fastcmp(word, "HORNTIME"))
{
followers[numfollowers].horntime = (tic_t)get_number(word2);
}
else
{
deh_warning("Follower %d: unknown word '%s'", numfollowers, word);
@ -3980,6 +3996,7 @@ if ((signed)followers[numfollowers].field < threshold) \
FALLBACK(bobamp, "BOBAMP", 0, 0);
FALLBACK(bobspeed, "BOBSPEED", 0, 0);
FALLBACK(hitconfirmtime, "HITCONFIRMTIME", 1, 1);
FALLBACK(horntime, "HORNTIME", 1, 1);
FALLBACK(scale, "SCALE", 1, 1); // No null/negative scale
FALLBACK(bubblescale, "BUBBLESCALE", 0, 0); // No negative scale
@ -4011,6 +4028,7 @@ if (!followers[numfollowers].field) \
NOSTATE(losestate, "LOSESTATE");
NOSTATE(winstate, "WINSTATE");
NOSTATE(hitconfirmstate, "HITCONFIRMSTATE");
NOSTATE(hornstate, "HORNSTATE");
#undef NOSTATE
CONS_Printf("Added follower '%s'\n", dname);

View file

@ -1370,6 +1370,7 @@ struct int_const_s const INT_CONST[] = {
{"BT_FORWARD",BT_FORWARD},
{"BT_BACKWARD",BT_BACKWARD},
{"BT_LOOKBACK",BT_LOOKBACK},
{"BT_HORN",BT_HORN},
{"BT_CUSTOM1",BT_CUSTOM1}, // Lua customizable
{"BT_CUSTOM2",BT_CUSTOM2}, // Lua customizable
{"BT_CUSTOM3",BT_CUSTOM3}, // Lua customizable
@ -1545,6 +1546,7 @@ struct int_const_s const INT_CONST[] = {
{"GC_CUSTOM3",gc_custom3},
{"GC_RESPAWN",gc_respawn},
{"GC_DIRECTOR",gc_director},
{"GC_HORNCODE",gc_horncode},
{"NUM_GAMECONTROLS",num_gamecontrols},
// screen.h constants

View file

@ -924,6 +924,10 @@ const char *blancredits[] = {
"\"Chearii\"",
"\"hayaunderscore\" aka \"DeltaKanyx\"",
"\"Guilmon35249vr\"",
"Vivian \"toaster\" Grannell", // Horncode
"AJ \"Tyron\" Martinez", // Horncode
"\"Superstarxalien\"", // Horncode
"\"Freaky Mutant Man\"", // Color profiles menu
"",
"\1Item Design",
"\"NepDisk\"",
@ -937,6 +941,9 @@ const char *blancredits[] = {
"\"luna\"",
"\"White Mage (guy who picked up controller)\"",
"\"minenice\"",
"\"StarrydustNova\"",
"\"merritt\"",
"\"Sunflower\" aka \"AnimeSonic\"",
"",
"\1New Item Art",
"\"Spee\"",
@ -957,9 +964,13 @@ const char *blancredits[] = {
"\"Miguelius256\"",
"\"Toddoesstuff\"",
"\"RetroStation\"",
"\"StarrydustNova\"",
"\"joshyflip\"",
"",
"\1New Misc Art",
"\"scizor300\"",
"\"StarrydustNova\"", // Per some indev talks. Can't reveal the secrets yet!
"\"joshyflip\"", // Per some indev talks. Can't reveal the secrets yet!
"",
"\1Additional Assets",
"Sonic Team Jr.",
@ -1030,11 +1041,14 @@ const char *blancredits[] = {
"\"PX8916\"",
"\"Tom\"",
"\"Phoenix\"",
"\"StarrydustNova\"",
"\"Prada\"",
"",
"\1Special Thanks",
"\"merritt\"",
"\"luna\"",
"\"Sunflower\" aka \"AnimeSonic\"",
"\"StarrydustNova\"",
"Sunflower's Garden",
"The Moe Mansion and Birdhouse Team",
"SRB2Kart Saturn Contributors",

View file

@ -1579,6 +1579,12 @@ void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
cmd->buttons |= BT_LOOKBACK;
}
// horn with any button/key
if (G_PlayerInputDown(forplayer, gc_horncode, false, DEADZONE_BUTTON))
{
cmd->buttons |= BT_HORN;
}
// Lua scriptable buttons
if (G_PlayerInputDown(forplayer, gc_custom1, false, DEADZONE_BUTTON))
cmd->buttons |= BT_CUSTOM1;

View file

@ -154,6 +154,7 @@ INT32 gamecontroldefault[num_gamecontrols][MAXINPUTMAPPING] = {
[gc_brake ] = {'d', KEY_JOY1+1 }, // B
[gc_fire ] = {KEY_SPACE, KEY_JOY1+9, KEY_AXIS1+8}, // LB, LT
[gc_lookback ] = {KEY_LSHIFT, KEY_JOY1+2 }, // X
[gc_horncode ] = {'r', KEY_JOY1+8 }, // R-Stick Click
[gc_pause ] = {KEY_PAUSE, KEY_JOY1+4 }, // Back
[gc_systemmenu ] = { KEY_JOY1+6 }, // Start
@ -509,6 +510,7 @@ static const char *gamecontrolname[num_gamecontrols] =
"custom3",
"respawn",
"director",
"horncode",
};
#define NUMKEYNAMES (sizeof (keynames)/sizeof (keyname_t))

View file

@ -88,6 +88,7 @@ typedef enum
gc_custom3, // Lua scriptable
gc_respawn,
gc_director,
gc_horncode,
num_gamecontrols
} gamecontrols_e;

View file

@ -874,3 +874,6 @@ _(RAINBOWDASHRING)
// Sneaker Panels
_(SNEAKERPANEL)
_(SNEAKERPANELSPAWNER)
// Horncode
_(FOLLOWERHORN)

View file

@ -1017,3 +1017,6 @@ _(sysmsg)
// Dash Rings
_(dashr)
_(rainbr)
// Horncode
_(horn00)

View file

@ -656,5 +656,8 @@ _(AWBT)
// Recovery Spin Skid
_(RCSP)
// Horncode
_(FHRN)
// First person view sprites; this is a sprite so that it can be replaced by a specialized MD2 draw later
_(VIEW)

View file

@ -3624,3 +3624,7 @@ _(ALTSHRINK_ARROWBULLET)
// Recovery Spin Skid
_(RECSPIN_SKID)
// Horncode
_(HORNCODE)
_(HORNCODE_ANGRY)
_(HORNCODE_HAPPY)

View file

@ -11,6 +11,8 @@
#include "r_skins.h"
#include "p_local.h"
#include "p_mobj.h"
#include "s_sound.h"
#include "m_cond.h"
INT32 numfollowers = 0;
follower_t followers[MAXSKINS];
@ -243,6 +245,32 @@ static void K_UpdateFollowerState(mobj_t *f, statenum_t state, followerstate_t t
}
}
/*--------------------------------------------------
static void K_UpdateFollowerMood(mobj_t *f, followermood_t mood, tic_t time)
Sets a follower object's mood and time before returning to a normal mood.
Input Arguments:-
f - The follower's mobj_t.
mood - The mood to set.
time - The mood's duration.
Return:-
None
--------------------------------------------------*/
static void K_UpdateFollowerMood(mobj_t *f, followermood_t mood, tic_t time)
{
if (f == NULL || P_MobjWasRemoved(f) == true)
{
// safety net
return;
}
f->extravalue3 = mood;
f->cvmem = (INT32)(time);
}
/*--------------------------------------------------
void K_HandleFollower(player_t *player)
@ -565,12 +593,27 @@ void K_HandleFollower(player_t *player)
// However with how the code is factored, this is just a special case of S_INVISBLE to avoid having to add other player variables.
// Mood system
// For now, all this does is change the VFX generated when you honk your horn.
if (player->follower->cvmem && (player->follower->extravalue3 != FOLLOWERMOOD_NORMAL))
{
// Tick down the mood timer
player->follower->cvmem--;
// Return to our normal mood
if (player->follower->cvmem == 0)
{
player->follower->extravalue3 = FOLLOWERMOOD_NORMAL;
}
}
// handle follower animations. Could probably be better...
// hurt or dead
if (P_PlayerInPain(player) == true || player->mo->state == &states[S_KART_SPINOUT] || player->mo->health <= 0)
{
// cancel hit confirm.
// cancel hit confirm / horn
player->follower->movecount = 0;
player->follower->reactiontime = 0;
// spin out
player->follower->angle = player->drawangle;
@ -582,6 +625,11 @@ void K_HandleFollower(player_t *player)
// if dead, follow the player's z momentum exactly so they both look like they die at the same speed.
player->follower->momz = player->mo->momz;
}
else
{
// Not dead; get mad on the player's behalf.
K_UpdateFollowerMood(player->follower, FOLLOWERMOOD_ANGRY, (3 * TICRATE / 2));
}
}
else if (player->exiting)
{
@ -600,8 +648,14 @@ void K_HandleFollower(player_t *player)
else if (player->follower->movecount)
{
K_UpdateFollowerState(player->follower, fl.hitconfirmstate, FOLLOWERSTATE_HITCONFIRM);
K_UpdateFollowerMood(player->follower, FOLLOWERMOOD_HAPPY, (3 * TICRATE / 2));
player->follower->movecount--;
}
else if (player->follower->reactiontime)
{
K_UpdateFollowerState(player->follower, fl.hornstate, FOLLOWERSTATE_HORN);
player->follower->reactiontime--;
}
else if (player->speed > 10*player->mo->scale) // animation for moving fast enough
{
K_UpdateFollowerState(player->follower, fl.followstate, FOLLOWERSTATE_FOLLOW);
@ -610,5 +664,135 @@ void K_HandleFollower(player_t *player)
{
K_UpdateFollowerState(player->follower, fl.idlestate, FOLLOWERSTATE_IDLE);
}
// Horncode
if (P_MobjWasRemoved(player->follower->hprev) == false)
{
mobj_t *honk = player->follower->hprev;
honk->flags2 &= ~MF2_AMBUSH;
honk->color = player->skincolor;
P_MoveOrigin(
honk,
player->follower->x,
player->follower->y,
player->follower->z + player->follower->height
);
K_FlipFromObject(honk, player->follower);
honk->angle = R_PointToAngle2(
player->mo->x,
player->mo->y,
player->follower->x,
player->follower->y
);
honk->destscale = (2*player->mo->scale)/3;
fixed_t offsetamount = 0;
if (honk->fuse > 1)
{
offsetamount = (honk->fuse-1)*honk->destscale/2;
if (leveltime & 1)
offsetamount = -offsetamount;
}
else if (S_SoundPlaying(honk, fl.hornsound))
{
honk->fuse++;
}
honk->sprxoff = P_ReturnThrustX(honk, honk->angle, offsetamount);
honk->spryoff = P_ReturnThrustY(honk, honk->angle, offsetamount);
honk->sprzoff = -honk->sprxoff;
}
}
}
/*--------------------------------------------------
void K_FollowerHornTaunt(player_t *taunter, player_t *victim)
See header file for description.
--------------------------------------------------*/
void K_FollowerHornTaunt(player_t *taunter, player_t *victim)
{
if (
(cv_karthorns.value == 0)
|| taunter == NULL
|| victim == NULL
|| taunter->followerskin < 0
|| taunter->followerskin >= numfollowers
|| (P_IsLocalPlayer(victim) == false && cv_karthorns.value != 2)
|| P_MobjWasRemoved(taunter->mo) == true
|| P_MobjWasRemoved(taunter->follower) == true
)
return;
const follower_t *fl = &followers[taunter->followerskin];
const boolean tasteful = (taunter->karthud[khud_taunthorns] == 0);
if (tasteful || cv_karthorns.value == 2)
{
mobj_t *honk = taunter->follower->hprev;
const fixed_t desiredscale = (2*taunter->mo->scale)/3;
if (P_MobjWasRemoved(honk) == true)
{
honk = P_SpawnMobj(
taunter->follower->x,
taunter->follower->y,
taunter->follower->z + taunter->follower->height,
MT_FOLLOWERHORN
);
if (P_MobjWasRemoved(honk) == true)
return; // Permit lua override of horn production
// Set the horn icon based on the follower's mood.
switch (taunter->follower->extravalue3)
{
case FOLLOWERMOOD_ANGRY:
P_SetMobjState(honk, S_HORNCODE_ANGRY);
break;
case FOLLOWERMOOD_HAPPY:
P_SetMobjState(honk, S_HORNCODE_HAPPY);
default:
break;
}
P_SetTarget(&taunter->follower->hprev, honk);
P_SetTarget(&honk->target, taunter->follower);
K_FlipFromObject(honk, taunter->follower);
honk->color = taunter->skincolor;
honk->angle = honk->old_angle = R_PointToAngle2(
taunter->mo->x,
taunter->mo->y,
taunter->follower->x,
taunter->follower->y
);
}
// Only do for the first activation this tic.
if (!(honk->flags2 & MF2_AMBUSH))
{
honk->destscale = desiredscale;
P_SetScale(honk, (11*desiredscale)/10);
honk->fuse = TICRATE/2;
honk->renderflags |= RF_DONTDRAW;
taunter->follower->reactiontime = fl->horntime; // reactiontime is used to play the horn animation for followers.
S_StartSound(taunter->follower, fl->hornsound);
honk->flags2 |= MF2_AMBUSH;
}
honk->renderflags &= ~K_GetPlayerDontDrawFlag(victim);
}
}

View file

@ -45,9 +45,18 @@ typedef enum
FOLLOWERSTATE_WIN,
FOLLOWERSTATE_LOSE,
FOLLOWERSTATE_HITCONFIRM, // Uses movecount as a timer for how long to play this state.
FOLLOWERSTATE_HORN, // Uses reactiontime as a timer for how long to play this state.
FOLLOWERSTATE__MAX
} followerstate_t;
typedef enum
{
FOLLOWERMOOD_NORMAL, // Default mood, produces a ++ symbol when you honk.
FOLLOWERMOOD_HAPPY, // Happy mood (recent hitconfirm, won race), produces a ♪ symbol when you honk.
FOLLOWERMOOD_ANGRY, // Angry/upset mood (taking damage, recently took damage), produces a 💢 symbol when you honk.
FOLLOWERMOOD__MAX
} followermood_t;
//
// We'll define these here because they're really just a mobj that'll follow some rules behind a player
//
@ -89,6 +98,10 @@ struct follower_t
statenum_t losestate; // state when the player has lost
statenum_t hitconfirmstate; // state for hit confirm
tic_t hitconfirmtime; // time to keep the above playing for
statenum_t hornstate; // state for pressing horn
tic_t horntime; // time to keep the above playing for
sfxenum_t hornsound; // Press (B) to announce you are pressing (B)
};
extern INT32 numfollowers;
@ -179,6 +192,21 @@ UINT16 K_GetEffectiveFollowerColor(UINT16 followercolor, follower_t *follower, U
void K_HandleFollower(player_t *player);
/*--------------------------------------------------
void K_FollowerHornTaunt(player_t *taunter, player_t *victim)
Plays horn and spawns object (MOSTLY non-netsynced)
Input Arguments:-
taunter - Source player with a follower
victim - Player that hears and sees the honk
Return:-
None
--------------------------------------------------*/
void K_FollowerHornTaunt(player_t *taunter, player_t *victim);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -246,6 +246,7 @@ void K_RegisterKartStuff(void)
CV_RegisterVar(&cv_kartspeedometer);
CV_RegisterVar(&cv_kartvoices);
CV_RegisterVar(&cv_karthitemdialog);
CV_RegisterVar(&cv_karthorns);
CV_RegisterVar(&cv_kartbot);
CV_RegisterVar(&cv_kartbot_cap);
CV_RegisterVar(&cv_kartbot_modifiermax);
@ -1541,6 +1542,44 @@ void K_SpawnBumpEffect(mobj_t *mo)
S_StartSound(mo, sfx_s3k49);
}
static void K_HonkFollowerHorn(player_t *honkPlayer)
{
const boolean tasteful = (honkPlayer->karthud[khud_taunthorns] == 0);
UINT8 i;
// Loop through all players and make them hear your honking!
for (i = 0; i < MAXPLAYERS; i++)
{
player_t *p;
if (!playeringame[i])
{
// Invalid player
continue;
}
p = &players[i];
if (!p->mo || P_MobjWasRemoved(p->mo))
{
// Invalid mobj
continue;
}
if (p->spectator)
{
// Not playing
continue;
}
K_FollowerHornTaunt(honkPlayer, p);
}
if (tasteful && honkPlayer->karthud[khud_taunthorns] < 2*TICRATE)
honkPlayer->karthud[khud_taunthorns] = 2*TICRATE;
}
static SINT8 K_GlanceAtPlayers(player_t *glancePlayer)
{
const fixed_t maxdistance = FixedMul(1280 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed));
@ -6400,6 +6439,9 @@ void K_KartPlayerHUDUpdate(player_t *player)
if (player->karthud[khud_tauntvoices])
player->karthud[khud_tauntvoices]--;
if (player->karthud[khud_taunthorns])
player->karthud[khud_taunthorns]--;
if (gametyperules & GTR_RINGS)
{
if ((K_RingsActive() == true))
@ -7689,6 +7731,14 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
// Do a funny hop!
K_QuiteSaltyHop(player);
// Honk your horn. Beep beep!
const boolean honking = ((cmd->buttons & BT_HORN) == BT_HORN);
if (honking)
{
K_HonkFollowerHorn(player);
}
player->prevonground = P_IsObjectOnGround(player->mo);
// Update SPB timer...

View file

@ -35,6 +35,7 @@ enum follower {
follower_anglelag,
follower_bobamp,
follower_bobspeed,
follower_hornsound,
// states
follower_idlestate,
follower_followstate,
@ -43,6 +44,8 @@ enum follower {
follower_losestate,
follower_hitconfirmstate,
follower_hitconfirmtime,
follower_hornstate,
follower_horntime,
//
};
static const char *const follower_opt[] = {
@ -61,6 +64,7 @@ static const char *const follower_opt[] = {
"anglelag",
"bobamp",
"bobspeed",
"hornsound",
// states
"idlestate",
"followstate",
@ -69,6 +73,8 @@ static const char *const follower_opt[] = {
"losestate",
"hitconfirmstate",
"hitconfirmtime",
"hornstate",
"horntime",
//
NULL
};
@ -130,6 +136,9 @@ static int follower_get(lua_State *L)
case follower_bobspeed:
lua_pushinteger(L, follower->bobspeed);
break;
case follower_hornsound:
lua_pushinteger(L, follower->hornsound);
break;
case follower_idlestate:
lua_pushinteger(L, follower->idlestate);
break;
@ -151,6 +160,12 @@ static int follower_get(lua_State *L)
case follower_hitconfirmtime:
lua_pushinteger(L, follower->hitconfirmtime);
break;
case follower_hornstate:
lua_pushinteger(L, follower->hornstate);
break;
case follower_horntime:
lua_pushinteger(L, follower->horntime);
break;
}
return 1;
}

View file

@ -4499,7 +4499,9 @@ void P_SaveNetGame(savebuffer_t *save, boolean resending)
mobj = (mobj_t *)th;
if (UNLIKELY(mobj->type == MT_HOOP || mobj->type == MT_HOOPCOLLIDE || mobj->type == MT_HOOPCENTER
// MT_SPARK: used for debug stuff
|| mobj->type == MT_SPARK || mobj->flags2 & MF2_DONTSYNC))
|| mobj->type == MT_SPARK
// MT_FOLLOWERHORN: So it turns out hornmod is fundamentally incompatible with netsync
|| mobj->type == MT_FOLLOWERHORN || mobj->flags2 & MF2_DONTSYNC))
continue;
mobj->mobjnum = i++;
}