From 21849ef1cedb49d56ba52348ae2ae3ba0a534a4f Mon Sep 17 00:00:00 2001 From: toaster Date: Fri, 30 Jan 2026 03:09:11 -0500 Subject: [PATCH 1/8] Horncode A much more focused replacement for Hornmod, specc'd out by Tyron and Oni working together and implemented by the author of this commit because it's pretty funny. - Followers have `hornsound` in their SOC configuration. - The default sound for all followers without a provided one is sfx_horn00. - They'll play this sound if you use lookback with one following you, and there's nearby players to get the player looking all the way around. - Only the players who are successfully considered for lookback will hear it. - Has a v1-like visual with less randomisation, but still netsynced. - Also controlled by the cvar `taunthorns`, which, like `tauntvoices`, takes "Tasteful" (default), "Meme", and "Off". TODO: make the condition for horn a little delayed, so you have to hold lookback for a little bit. --- src/d_netcmd.c | 3 +- src/d_netcmd.h | 1 + src/d_player.h | 1 + src/deh_soc.c | 5 ++ src/info/mobjs.h | 3 ++ src/info/sounds.h | 3 ++ src/info/sprites.h | 3 ++ src/info/states.h | 2 + src/k_follower.c | 122 +++++++++++++++++++++++++++++++++++++++++++++ src/k_follower.h | 17 +++++++ src/k_kart.c | 24 ++++++++- src/p_saveg.c | 4 +- 12 files changed, 184 insertions(+), 4 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index f13b53579..3348acaaf 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -436,7 +436,8 @@ consvar_t cv_kartgametypepreference = CVAR_INIT ("kartgametypepreference", "None static CV_PossibleValue_t kartspeedometer_cons_t[] = {{0, "Off"}, {1, "Kilometers"}, {2, "Miles"}, {3, "Fracunits"}, {4, "Percentage"}, {0, NULL}}; 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_kartvoices = CVAR_INIT ("tauntvoices", "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"}, diff --git a/src/d_netcmd.h b/src/d_netcmd.h index 8b5b1cc66..ad422ee8d 100644 --- a/src/d_netcmd.h +++ b/src/d_netcmd.h @@ -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; diff --git a/src/d_player.h b/src/d_player.h index 0e3d9193c..1842884a5 100644 --- a/src/d_player.h +++ b/src/d_player.h @@ -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 diff --git a/src/deh_soc.c b/src/deh_soc.c index ae4994be6..275c92791 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3763,6 +3763,7 @@ void readfollower(MYFILE *f) followers[numfollowers].bobamp = 4*FRACUNIT; followers[numfollowers].hitconfirmtime = TICRATE; followers[numfollowers].defaultcolor = FOLLOWERCOLOR_MATCH; + followers[numfollowers].hornsound = sfx_horn00; strcpy(followers[numfollowers].icon, "MISSING"); do @@ -3829,6 +3830,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); diff --git a/src/info/mobjs.h b/src/info/mobjs.h index a45912f63..a7e4a6fcb 100644 --- a/src/info/mobjs.h +++ b/src/info/mobjs.h @@ -874,3 +874,6 @@ _(RAINBOWDASHRING) // Sneaker Panels _(SNEAKERPANEL) _(SNEAKERPANELSPAWNER) + +// Horncode +_(FOLLOWERHORN) diff --git a/src/info/sounds.h b/src/info/sounds.h index 601248ee4..2182f9703 100644 --- a/src/info/sounds.h +++ b/src/info/sounds.h @@ -1017,3 +1017,6 @@ _(sysmsg) // Dash Rings _(dashr) _(rainbr) + +// Horncode +_(horn00) diff --git a/src/info/sprites.h b/src/info/sprites.h index 74b3bfa27..5f8fb0b5e 100644 --- a/src/info/sprites.h +++ b/src/info/sprites.h @@ -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) diff --git a/src/info/states.h b/src/info/states.h index 7440fdeff..f4c659848 100644 --- a/src/info/states.h +++ b/src/info/states.h @@ -3624,3 +3624,5 @@ _(ALTSHRINK_ARROWBULLET) // Recovery Spin Skid _(RECSPIN_SKID) +// Horncode +_(HORNCODE) diff --git a/src/k_follower.c b/src/k_follower.c index b5706319d..58ae90fd0 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -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]; @@ -610,5 +612,125 @@ 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 + ); + + // TODO (yama): Honk icons based on a follower's "emotion" + // (♪ for hitconfirm honks, 💢 for mid-damage honks) + + if (P_MobjWasRemoved(honk) == true) + return; // Permit lua override of horn production + + 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; + + S_StartSound(taunter->follower, fl->hornsound); + + honk->flags2 |= MF2_AMBUSH; + } + + honk->renderflags &= ~K_GetPlayerDontDrawFlag(victim); } } diff --git a/src/k_follower.h b/src/k_follower.h index 41efae7c0..28dae9929 100644 --- a/src/k_follower.h +++ b/src/k_follower.h @@ -89,6 +89,8 @@ 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 + + sfxenum_t hornsound; // Press (B) to announce you are pressing (B) }; extern INT32 numfollowers; @@ -179,6 +181,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 diff --git a/src/k_kart.c b/src/k_kart.c index 007eec8f4..dcc1a9e94 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -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,7 +1542,7 @@ void K_SpawnBumpEffect(mobj_t *mo) S_StartSound(mo, sfx_s3k49); } -static SINT8 K_GlanceAtPlayers(player_t *glancePlayer) +static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) { const fixed_t maxdistance = FixedMul(1280 * mapobjectscale, K_GetKartGameSpeedScalar(gamespeed)); const angle_t blindSpotSize = ANG10; // ANG5 @@ -1623,9 +1624,24 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer) // That poses a limitation if there's an equal number of targets on both sides... // In that case, we'll pick the last chosen glance direction. lastValidGlance = dir; + + if (horn == true) + { + K_FollowerHornTaunt(glancePlayer, p); + } } } + if (horn == true && lastValidGlance != 0) + { + const boolean tasteful = (glancePlayer->karthud[khud_taunthorns] == 0); + + K_FollowerHornTaunt(glancePlayer, glancePlayer); + + if (tasteful && glancePlayer->karthud[khud_taunthorns] < 2*TICRATE) + glancePlayer->karthud[khud_taunthorns] = 2*TICRATE; + } + if (glanceDir > 0) { return 1; @@ -1783,7 +1799,8 @@ void K_KartMoveAnimation(player_t *player) { // Only try glancing if you're driving straight. // This avoids all-players loops when we don't need it. - destGlanceDir = K_GlanceAtPlayers(player); + const boolean horn = lookback && !(player->pflags & PF_GAINAX); + destGlanceDir = K_GlanceAtPlayers(player, horn); if (lookback == true) { @@ -6400,6 +6417,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)) diff --git a/src/p_saveg.c b/src/p_saveg.c index d48571bfe..cd3bc4a5b 100644 --- a/src/p_saveg.c +++ b/src/p_saveg.c @@ -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++; } From ee89cc251d408925386e581f1bdb2a2f11dd5ee8 Mon Sep 17 00:00:00 2001 From: yamamama Date: Fri, 30 Jan 2026 09:23:40 -0500 Subject: [PATCH 2/8] I_Error(): 1 warning in the SOC lump --- src/d_netcmd.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 3348acaaf..e66c8a371 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -436,7 +436,7 @@ consvar_t cv_kartgametypepreference = CVAR_INIT ("kartgametypepreference", "None static CV_PossibleValue_t kartspeedometer_cons_t[] = {{0, "Off"}, {1, "Kilometers"}, {2, "Miles"}, {3, "Fracunits"}, {4, "Percentage"}, {0, NULL}}; 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 ("tauntvoices", "Tasteful", CV_SAVE, kartvoices_cons_t, 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[] = { From 055a93b654e89079bbf8900811187db9853458a1 Mon Sep 17 00:00:00 2001 From: yamamama Date: Fri, 30 Jan 2026 10:24:39 -0500 Subject: [PATCH 3/8] Dedicated horn button From indev talks: it'd be better to bind horns to a dedicated button, than for it to occupy more "important" inputs like item/ring usage and looking backwards --- src/d_ticcmd.h | 3 ++- src/deh_tables.c | 2 ++ src/g_game.c | 6 +++++ src/g_input.c | 2 ++ src/g_input.h | 1 + src/k_kart.c | 66 +++++++++++++++++++++++++++++++++++------------- 6 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/d_ticcmd.h b/src/d_ticcmd.h index adf050057..a5f700ad2 100644 --- a/src/d_ticcmd.h +++ b/src/d_ticcmd.h @@ -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, diff --git a/src/deh_tables.c b/src/deh_tables.c index a6012482b..f44e346ba 100644 --- a/src/deh_tables.c +++ b/src/deh_tables.c @@ -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 diff --git a/src/g_game.c b/src/g_game.c index 71adedadb..0d15b295b 100644 --- a/src/g_game.c +++ b/src/g_game.c @@ -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; diff --git a/src/g_input.c b/src/g_input.c index f0f0e186a..d04249e38 100644 --- a/src/g_input.c +++ b/src/g_input.c @@ -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)) diff --git a/src/g_input.h b/src/g_input.h index 39bba1eb8..502be04ce 100644 --- a/src/g_input.h +++ b/src/g_input.h @@ -88,6 +88,7 @@ typedef enum gc_custom3, // Lua scriptable gc_respawn, gc_director, + gc_horncode, num_gamecontrols } gamecontrols_e; diff --git a/src/k_kart.c b/src/k_kart.c index dcc1a9e94..8f4ea459b 100644 --- a/src/k_kart.c +++ b/src/k_kart.c @@ -1542,7 +1542,45 @@ void K_SpawnBumpEffect(mobj_t *mo) S_StartSound(mo, sfx_s3k49); } -static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) +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)); const angle_t blindSpotSize = ANG10; // ANG5 @@ -1624,24 +1662,9 @@ static SINT8 K_GlanceAtPlayers(player_t *glancePlayer, boolean horn) // That poses a limitation if there's an equal number of targets on both sides... // In that case, we'll pick the last chosen glance direction. lastValidGlance = dir; - - if (horn == true) - { - K_FollowerHornTaunt(glancePlayer, p); - } } } - if (horn == true && lastValidGlance != 0) - { - const boolean tasteful = (glancePlayer->karthud[khud_taunthorns] == 0); - - K_FollowerHornTaunt(glancePlayer, glancePlayer); - - if (tasteful && glancePlayer->karthud[khud_taunthorns] < 2*TICRATE) - glancePlayer->karthud[khud_taunthorns] = 2*TICRATE; - } - if (glanceDir > 0) { return 1; @@ -1799,8 +1822,7 @@ void K_KartMoveAnimation(player_t *player) { // Only try glancing if you're driving straight. // This avoids all-players loops when we don't need it. - const boolean horn = lookback && !(player->pflags & PF_GAINAX); - destGlanceDir = K_GlanceAtPlayers(player, horn); + destGlanceDir = K_GlanceAtPlayers(player); if (lookback == true) { @@ -7709,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... From 2221d6b02709296630995439ba4fe8d153034065 Mon Sep 17 00:00:00 2001 From: Superstarxalien Date: Tue, 20 Jan 2026 21:09:50 -0400 Subject: [PATCH 4/8] Add "horn" state for followers --- src/deh_soc.c | 13 +++++++++++++ src/k_follower.c | 9 ++++++++- src/k_follower.h | 3 +++ src/lua_followerlib.c | 10 ++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/deh_soc.c b/src/deh_soc.c index 275c92791..f5f102b0c 100644 --- a/src/deh_soc.c +++ b/src/deh_soc.c @@ -3762,6 +3762,7 @@ 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"); @@ -3919,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); @@ -3985,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 @@ -4016,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); diff --git a/src/k_follower.c b/src/k_follower.c index 58ae90fd0..d02762a1d 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -571,8 +571,9 @@ void K_HandleFollower(player_t *player) // 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; @@ -604,6 +605,11 @@ void K_HandleFollower(player_t *player) K_UpdateFollowerState(player->follower, fl.hitconfirmstate, FOLLOWERSTATE_HITCONFIRM); 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); @@ -725,6 +731,7 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim) 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); diff --git a/src/k_follower.h b/src/k_follower.h index 28dae9929..6cb5da15a 100644 --- a/src/k_follower.h +++ b/src/k_follower.h @@ -45,6 +45,7 @@ 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; @@ -89,6 +90,8 @@ 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) }; diff --git a/src/lua_followerlib.c b/src/lua_followerlib.c index 6bea5ea9c..e982cbf5a 100644 --- a/src/lua_followerlib.c +++ b/src/lua_followerlib.c @@ -43,6 +43,8 @@ enum follower { follower_losestate, follower_hitconfirmstate, follower_hitconfirmtime, + follower_hornstate, + follower_horntime, // }; static const char *const follower_opt[] = { @@ -69,6 +71,8 @@ static const char *const follower_opt[] = { "losestate", "hitconfirmstate", "hitconfirmtime", + "hornstate", + "horntime", // NULL }; @@ -151,6 +155,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; } From addcb2b85311e6982f1d441f39bb43f19d2aa0b6 Mon Sep 17 00:00:00 2001 From: yamamama Date: Fri, 30 Jan 2026 11:30:05 -0500 Subject: [PATCH 5/8] Update the credits --- src/f_finale.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/f_finale.c b/src/f_finale.c index c22360a99..729ef3cbb 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -924,6 +924,10 @@ const char *blancredits[] = { "\"Chearii\"", "\"hayaunderscore\" aka \"DeltaKanyx\"", "\"Guilmon35249vr\"", + "Viv \"toastergrl\" Grannell", + "AJ \"Tyron\' Martinez", + "\"Superstarxalien\"", + "\"Freaky Mutant Man\"", "", "\1Item Design", "\"NepDisk\"", From d4bd7b68cfb052f435ba93caf0ea1275435dbf46 Mon Sep 17 00:00:00 2001 From: yamamama Date: Fri, 30 Jan 2026 11:54:32 -0500 Subject: [PATCH 6/8] Fix up some inconsistencies and errors, update the credits (again, again) About TIME I get an excuse to chip at my credits backlog! --- src/f_finale.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/f_finale.c b/src/f_finale.c index 729ef3cbb..cf2311fe4 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -924,10 +924,10 @@ const char *blancredits[] = { "\"Chearii\"", "\"hayaunderscore\" aka \"DeltaKanyx\"", "\"Guilmon35249vr\"", - "Viv \"toastergrl\" Grannell", - "AJ \"Tyron\' Martinez", - "\"Superstarxalien\"", - "\"Freaky Mutant Man\"", + "Vivian \"toaster\" Grannell", // Horncode + "AJ \"Tyron\" Martinez", // Horncode + "\"Superstarxalien\"", // Horncode + "\"Freaky Mutant Man\"", // Color profiles menu "", "\1Item Design", "\"NepDisk\"", @@ -941,6 +941,9 @@ const char *blancredits[] = { "\"luna\"", "\"White Mage (guy who picked up controller)\"", "\"minenice\"", + "\"StarrydustNova\"", + "\"merritt\"", + "\"Sunflower\" aka \"AnimeSonic\"", "", "\1New Item Art", "\"Spee\"", @@ -961,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.", @@ -1034,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", From 5efbc22a0123ca85bacd5c985a89f663ae8afdbd Mon Sep 17 00:00:00 2001 From: yamamama Date: Fri, 30 Jan 2026 12:06:56 -0500 Subject: [PATCH 7/8] Expose horn sounds to Lua --- src/lua_followerlib.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lua_followerlib.c b/src/lua_followerlib.c index e982cbf5a..2fded3882 100644 --- a/src/lua_followerlib.c +++ b/src/lua_followerlib.c @@ -35,6 +35,7 @@ enum follower { follower_anglelag, follower_bobamp, follower_bobspeed, + follower_hornsound, // states follower_idlestate, follower_followstate, @@ -63,6 +64,7 @@ static const char *const follower_opt[] = { "anglelag", "bobamp", "bobspeed", + "hornsound", // states "idlestate", "followstate", @@ -134,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; From 598f974fdc8843ff448f6b69a99f61db6f8e94f4 Mon Sep 17 00:00:00 2001 From: yamamama Date: Sat, 31 Jan 2026 21:56:15 -0500 Subject: [PATCH 8/8] Follower mood system * Followers get happy when you hit others, and angry when others hit you * The mood doesn't do much beyond change the horn's symbol --- src/info/states.h | 2 ++ src/k_follower.c | 61 ++++++++++++++++++++++++++++++++++++++++++++--- src/k_follower.h | 8 +++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/info/states.h b/src/info/states.h index f4c659848..067f91d6d 100644 --- a/src/info/states.h +++ b/src/info/states.h @@ -3626,3 +3626,5 @@ _(RECSPIN_SKID) // Horncode _(HORNCODE) +_(HORNCODE_ANGRY) +_(HORNCODE_HAPPY) diff --git a/src/k_follower.c b/src/k_follower.c index d02762a1d..529650d2d 100644 --- a/src/k_follower.c +++ b/src/k_follower.c @@ -245,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) @@ -567,6 +593,20 @@ 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) @@ -585,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) { @@ -603,6 +648,7 @@ 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) @@ -702,12 +748,21 @@ void K_FollowerHornTaunt(player_t *taunter, player_t *victim) MT_FOLLOWERHORN ); - // TODO (yama): Honk icons based on a follower's "emotion" - // (♪ for hitconfirm honks, 💢 for mid-damage honks) - 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); diff --git a/src/k_follower.h b/src/k_follower.h index 6cb5da15a..e1f2590ec 100644 --- a/src/k_follower.h +++ b/src/k_follower.h @@ -49,6 +49,14 @@ typedef enum 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 //