Merge branch 'battle-fixes-five-nights' into 'master'

Battle Fixes at Freddy's (includes gameplay changes that affect Race too!)

Closes #879, #893, #900, #918, #906, #903, #913, #907, and #905

See merge request KartKrew/Kart!1837
This commit is contained in:
Oni 2024-01-14 07:41:31 +00:00
commit 271a5a44ed
19 changed files with 257 additions and 76 deletions

View file

@ -712,7 +712,7 @@ consvar_t cv_kartbot = UnsavedNetVar("bots", "Off").values({
{13,"Lv.MAX"},
});
consvar_t cv_kartbumpers = UnsavedNetVar("battlebumpers", "3").min_max(1, 12);
consvar_t cv_kartbumpers = UnsavedNetVar("battlebumpers", "3").min_max(0, 12);
void KartEliminateLast_OnChange(void);
consvar_t cv_karteliminatelast = UnsavedNetVar("eliminatelast", "Yes").yes_no().onchange(KartEliminateLast_OnChange);

View file

@ -2194,6 +2194,8 @@ void D_MapChange(UINT16 mapnum, INT32 newgametype, boolean pencoremode, boolean
void D_SetupVote(INT16 newgametype)
{
const UINT32 rules = gametypes[newgametype]->rules;
UINT8 buf[(VOTE_NUM_LEVELS * 2) + 4];
UINT8 *p = buf;
@ -2203,16 +2205,37 @@ void D_SetupVote(INT16 newgametype)
memset(votebuffer, UINT16_MAX, sizeof(votebuffer));
WRITEINT16(p, newgametype);
WRITEUINT8(p, ((cv_kartencore.value == 1) && (gametyperules & GTR_ENCORE)));
WRITEUINT8(p, ((cv_kartencore.value == 1) && (rules & GTR_ENCORE)));
WRITEUINT8(p, G_SometimesGetDifferentEncore());
UINT8 numPlayers = 0;
for (i = 0; i < MAXPLAYERS; ++i)
{
if (!playeringame[i] || players[i].spectator)
{
continue;
}
extern consvar_t cv_forcebots; // debug
if (!(rules & GTR_BOTS) && players[i].bot && !cv_forcebots.value)
{
// Gametype doesn't support bots
continue;
}
numPlayers++;
}
for (i = 0; i < VOTE_NUM_LEVELS; i++)
{
UINT16 m = G_RandMap(
UINT16 m = G_RandMapPerPlayerCount(
G_TOLFlag(newgametype),
prevmap, false,
(i < VOTE_NUM_LEVELS-1),
votebuffer
votebuffer,
numPlayers
);
votebuffer[i] = m;
WRITEUINT16(p, m);

View file

@ -1417,6 +1417,29 @@ void readlevelheader(MYFILE *f, char * name)
mapheaderinfo[num]->destroyforchallenge_size = j;
}
}
else if (fastcmp(word, "LOBBYSIZE"))
{
if (fastcmp(word2, "DUEL"))
{
mapheaderinfo[num]->playerLimit = 2;
}
else if (fastcmp(word2, "SMALL"))
{
mapheaderinfo[num]->playerLimit = 5;
}
else if (fastcmp(word2, "MEDIUM"))
{
mapheaderinfo[num]->playerLimit = 10;
}
else if (fastcmp(word2, "LARGE"))
{
mapheaderinfo[num]->playerLimit = 16;
}
else
{
deh_warning("Level header %d: invalid lobby size '%s'", num, word2);
}
}
else
deh_warning("Level header %d: unknown word '%s'", num, word);
}

View file

@ -501,6 +501,7 @@ struct mapheader_t
UINT8 levelselect; ///< Is this map available in the level select? If so, which map list is it available in?
UINT16 menuflags; ///< LF2_flags: options that affect record attack menus
UINT8 playerLimit; ///< This map does not appear in multiplayer vote if there are too many players
// Operational metadata
UINT16 levelflags; ///< LF_flags: merged booleans into one UINT16 for space, see below

View file

@ -1526,7 +1526,8 @@ boolean G_CouldView(INT32 playernum)
//
boolean G_CanView(INT32 playernum, UINT8 viewnum, boolean onlyactive)
{
if (!playeringame[playernum] || players[playernum].spectator)
// PF_ELIMINATED: Battle Overtime Barrier killed this player
if (!playeringame[playernum] || players[playernum].spectator || (players[playernum].pflags & PF_ELIMINATED))
{
return false;
}
@ -3499,7 +3500,7 @@ static UINT16 *g_allowedMaps = NULL;
static size_t g_randMapStack = 0;
#endif
UINT16 G_RandMap(UINT32 tolflags, UINT16 pprevmap, boolean ignoreBuffers, boolean callAgainSoon, UINT16 *extBuffer)
UINT16 G_RandMapPerPlayerCount(UINT32 tolflags, UINT16 pprevmap, boolean ignoreBuffers, boolean callAgainSoon, UINT16 *extBuffer, UINT8 numPlayers)
{
INT32 allowedMapsCount = 0;
INT32 extBufferCount = 0;
@ -3558,6 +3559,12 @@ tryAgain:
continue;
}
if (numPlayers > mapheaderinfo[i]->playerLimit)
{
// Too many players for this map.
continue;
}
// Only care about restrictions if the host is a listen server.
if (!dedicated)
{
@ -3665,6 +3672,11 @@ tryAgain:
return ret;
}
UINT16 G_RandMap(UINT32 tolflags, UINT16 pprevmap, boolean ignoreBuffers, boolean callAgainSoon, UINT16 *extBuffer)
{
return G_RandMapPerPlayerCount(tolflags, pprevmap, ignoreBuffers, callAgainSoon, extBuffer, 0);
}
void G_AddMapToBuffer(UINT16 map)
{
if (mapheaderinfo[map]->justPlayed == 0) // Started playing a new map.

View file

@ -277,6 +277,7 @@ FUNCMATH INT32 G_TicsToMilliseconds(tic_t tics);
UINT32 G_TOLFlag(INT32 pgametype);
UINT16 G_GetFirstMapOfGametype(UINT16 pgametype);
UINT16 G_RandMapPerPlayerCount(UINT32 tolflags, UINT16 pprevmap, boolean ignoreBuffers, boolean callAgainSoon, UINT16 *extBuffer, UINT8 numPlayers);
UINT16 G_RandMap(UINT32 tolflags, UINT16 pprevmap, boolean ignoreBuffers, boolean callAgainSoon, UINT16 *extBuffer);
void G_AddMapToBuffer(UINT16 map);

View file

@ -745,17 +745,6 @@ void K_LightningShieldAttack(mobj_t *actor, fixed_t size)
boolean K_BubbleShieldCollide(mobj_t *t1, mobj_t *t2)
{
if (t1->type == MT_PLAYER)
{
// Bubble Shield already has a hitbox, and it gets
// teleported every tic so the Bubble itself will
// always make contact with other objects.
//
// Therefore, we don't need a second, smaller hitbox
// on the player. It'll just cause unwanted hitlag.
return true;
}
if (t2->type == MT_PLAYER)
{
// Counter desyncs
@ -780,21 +769,29 @@ boolean K_BubbleShieldCollide(mobj_t *t1, mobj_t *t2)
}
else
{
if (!t2->threshold || t2->type == MT_DROPTARGET)
mobj_t *owner = t1->player ? t1 : t1->target;
if (t2->target != owner || !t2->threshold || t2->type == MT_DROPTARGET)
{
if (t1->player && K_PlayerGuard(t1->player))
{
K_KartSolidBounce(t1, t2);
K_DoPowerClash(t1, t2);
}
if (!t2->momx && !t2->momy)
{
t2->momz += (24*t2->scale) * P_MobjFlip(t2);
}
else
{
t2->momx = -4*t2->momx;
t2->momy = -4*t2->momy;
t2->momz = -4*t2->momz;
t2->momx = -6*t2->momx;
t2->momy = -6*t2->momy;
t2->momz = -6*t2->momz;
t2->angle += ANGLE_180;
}
if (t2->type == MT_JAWZ)
P_SetTarget(&t2->tracer, t2->target); // Back to the source!
P_SetTarget(&t2->target, owner); // Let the source reflect it back again!
t2->threshold = 10;
S_StartSound(t1, sfx_s3k44);
}
@ -825,8 +822,7 @@ boolean K_InstaWhipCollide(mobj_t *shield, mobj_t *victim)
{
player_t *victimPlayer = victim->player;
//if (victim != attacker && !P_PlayerInPain(victimPlayer) && victimPlayer->flashing == 0)
if (victim != attacker && victim->hitlag == 0)
if (victim != attacker && (P_PlayerInPain(victimPlayer) ? victim->hitlag == 0 : victimPlayer->flashing == 0))
{
// If both players have a whip, hits are order-of-execution dependent and that sucks.
// Player expectation is a clash here.

View file

@ -112,7 +112,9 @@ struct DirectorInfo
}
// pair finished? try the next one
if (players[playerstat[targetposition].sorted].exiting)
if (players[playerstat[targetposition].sorted].exiting ||
// Battle: player was killed by Overtime Barrier
(players[playerstat[targetposition].sorted].pflags & PF_ELIMINATED))
{
continue;
}
@ -137,12 +139,6 @@ struct DirectorInfo
break;
}
// if this is a splitscreen player, try next pair
if (P_IsDisplayPlayer(&players[target]))
{
continue;
}
// if we're certain the back half of the pair is actually in this position, try to switch
if (!players[target].positiondelay)
{

View file

@ -14,6 +14,7 @@
#include "doomdef.h"
#include "doomstat.h"
#include "k_battle.h"
#include "k_kart.h"
#include "m_random.h"
#include "p_local.h"
@ -195,6 +196,11 @@ static void K_SpawnHitLagEFX(mobj_t *victim, mobj_t *inflictor, mobj_t *source,
newScale = 3 * victim->destscale;
}
if ((gametyperules & GTR_BUMPERS) && battleprisons == false)
{
newScale = 3 * newScale / 4;
}
if (P_MobjWasRemoved(source) == false)
{
color = (source->player != NULL) ? source->player->skincolor : source->color;

View file

@ -2224,13 +2224,20 @@ struct PositionFacesInfo
void draw_1p();
void draw_4p_battle(int x, int y, INT32 flags);
UINT32 top_score() const { return players[rankplayer[0]].roundscore; }
bool near_goal() const { return g_pointlimit - 5 <= top_score(); }
player_t* top() const { return &players[rankplayer[0]]; }
UINT32 top_score() const { return top()->roundscore; }
bool near_goal() const
{
constexpr tic_t kThreshold = 5;
return std::max(kThreshold, g_pointlimit) - kThreshold <= top_score();
}
skincolornum_t vomit_color() const
{
if (!near_goal())
{
return SKINCOLOR_NONE;
return static_cast<skincolornum_t>(top()->skincolor);
}
constexpr int kCycleSpeed = 4;
@ -2308,7 +2315,11 @@ void PositionFacesInfo::draw_1p()
UINT32 skinflags;
if (gametyperules & GTR_POINTLIMIT) // playing battle
Y += (9*5) - 5; // <-- arbitrary calculation
{
Y += 40;
if (ranklines < 3)
Y -= 18;
}
else if (ranklines < 5)
Y += (9*ranklines);
else

View file

@ -3550,6 +3550,11 @@ UINT16 K_GetKartFlashing(const player_t *player)
{
UINT16 tics = flashingtics;
if (gametyperules & GTR_BUMPERS)
{
return 1;
}
if (player == NULL)
{
return tics;
@ -3559,16 +3564,6 @@ UINT16 K_GetKartFlashing(const player_t *player)
return tics;
}
void K_UpdateDamageFlashing(player_t *player, UINT16 tics)
{
if (gametyperules & GTR_BUMPERS)
{
return;
}
player->flashing = tics;
}
boolean K_PlayerShrinkCheat(const player_t *player)
{
return (
@ -8396,9 +8391,9 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
if (player->spinouttimer != 0)
{
if (( player->spinouttype & KSPIN_IFRAMES ) == 0)
K_UpdateDamageFlashing(player, 0);
player->flashing = 0;
else
K_UpdateDamageFlashing(player, K_GetKartFlashing(player));
player->flashing = K_GetKartFlashing(player);
}
if (player->spinouttimer)
@ -13333,6 +13328,11 @@ UINT32 K_PointLimitForGametype(void)
ptsCap += 4;
}
}
if (ptsCap > 20)
{
ptsCap = 20;
}
}
return ptsCap;

View file

@ -192,7 +192,6 @@ fixed_t K_GetKartSpeedFromStat(UINT8 kartspeed);
fixed_t K_GetKartSpeed(const player_t *player, boolean doboostpower, boolean dorubberbanding);
fixed_t K_GetKartAccel(const player_t *player);
UINT16 K_GetKartFlashing(const player_t *player);
void K_UpdateDamageFlashing(player_t *player, UINT16 tics);
boolean K_PlayerShrinkCheat(const player_t *player);
void K_UpdateShrinkCheat(player_t *player);
boolean K_KartKickstart(const player_t *player);

View file

@ -131,7 +131,7 @@ INT16 K_PowerLevelPlacementScore(player_t *player)
}
else
{
return player->score;
return player->roundscore;
}
}

View file

@ -1,6 +1,7 @@
#include <algorithm>
#include "../d_player.h"
#include "../k_battle.h"
#include "../k_objects.h"
#include "../m_fixed.h"
#include "../info.h"
@ -230,7 +231,10 @@ void Obj_SpawnGachaBomRebound(mobj_t* source, mobj_t* target)
x->color = target->color;
x->angle = angle;
P_InstaScale(x, 2 * x->scale);
if (!(gametyperules & GTR_BUMPERS) || battleprisons)
{
P_InstaScale(x, 2 * x->scale);
}
rebound_mode(x) = static_cast<int>(mode);
rebound_timer(x) = kReboundAcceptPause;

View file

@ -2812,6 +2812,34 @@ static void AddNullHitlag(player_t *player, tic_t oldHitlag)
}
}
static boolean P_FlashingException(const player_t *player, const mobj_t *inflictor)
{
if (!inflictor)
{
// Sector damage always behaves the same.
return false;
}
if (!P_IsKartItem(inflictor->type) && inflictor->type != MT_PLAYER)
{
// Exception only applies to player items.
// Also applies to players because of PvP collision.
// Lightning Shield also uses the player object as inflictor.
return false;
}
if (!P_PlayerInPain(player))
{
// Flashing tics is sometimes used in a way unrelated to damage.
// E.g. picking up a power-up gives you flashing tics.
// Respect this usage of flashing tics.
return false;
}
// Flashing tics are ignored.
return true;
}
/** Damages an object, which may or may not be a player.
* For melee attacks, source and inflictor are the same.
*
@ -3176,7 +3204,12 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
}
// DMG_EXPLODE excluded from flashtic checks to prevent dodging eggbox/SPB with weak spinout
if ((target->hitlag == 0 || allowcombo == false) && player->flashing > 0 && type != DMG_EXPLODE && type != DMG_STUMBLE && type != DMG_WHUMBLE)
if ((target->hitlag == 0 || allowcombo == false) &&
player->flashing > 0 &&
type != DMG_EXPLODE &&
type != DMG_STUMBLE &&
type != DMG_WHUMBLE &&
P_FlashingException(player, inflictor) == false)
{
// Post-hit invincibility
K_DoInstashield(player);
@ -3341,7 +3374,7 @@ boolean P_DamageMobj(mobj_t *target, mobj_t *inflictor, mobj_t *source, INT32 da
if (type != DMG_STUMBLE && type != DMG_WHUMBLE)
{
if (type != DMG_STING)
K_UpdateDamageFlashing(player, K_GetKartFlashing(player));
player->flashing = K_GetKartFlashing(player);
player->ringburst += ringburst;

View file

@ -522,6 +522,17 @@ static void P_DoFanAndGasJet(mobj_t *spring, mobj_t *object)
}
}
static boolean P_BubbleCanReflect(mobj_t *t1, mobj_t *t2)
{
return (t2->type == MT_ORBINAUT || t2->type == MT_JAWZ || t2->type == MT_GACHABOM
|| t2->type == MT_BANANA || t2->type == MT_EGGMANITEM || t2->type == MT_BALLHOG
|| t2->type == MT_SSMINE || t2->type == MT_LANDMINE || t2->type == MT_SINK
|| t2->type == MT_GARDENTOP
|| t2->type == MT_DROPTARGET
|| t2->type == MT_KART_LEFTOVER
|| (t2->type == MT_PLAYER && t1->target != t2));
}
//
// PIT_CheckThing
//
@ -993,16 +1004,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing)
if (tm.thing->type == MT_RANDOMITEM)
return BMIT_CONTINUE;
// Bubble Shield reflect
if (((thing->type == MT_BUBBLESHIELD && thing->target->player && thing->target->player->bubbleblowup)
|| (thing->player && thing->player->bubbleblowup))
&& (tm.thing->type == MT_ORBINAUT || tm.thing->type == MT_JAWZ || tm.thing->type == MT_GACHABOM
|| tm.thing->type == MT_BANANA || tm.thing->type == MT_EGGMANITEM || tm.thing->type == MT_BALLHOG
|| tm.thing->type == MT_SSMINE || tm.thing->type == MT_LANDMINE || tm.thing->type == MT_SINK
|| tm.thing->type == MT_GARDENTOP
|| tm.thing->type == MT_DROPTARGET
|| tm.thing->type == MT_KART_LEFTOVER
|| (tm.thing->type == MT_PLAYER && thing->target != tm.thing)))
if (tm.thing->type != MT_PLAYER && thing->player && K_PlayerGuard(thing->player) && P_BubbleCanReflect(thing, tm.thing))
{
// see if it went over / under
if (tm.thing->z > thing->z + thing->height)
@ -1012,15 +1014,7 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing)
return K_BubbleShieldCollide(thing, tm.thing) ? BMIT_CONTINUE : BMIT_ABORT;
}
else if (((tm.thing->type == MT_BUBBLESHIELD && tm.thing->target->player && tm.thing->target->player->bubbleblowup)
|| (tm.thing->player && tm.thing->player->bubbleblowup))
&& (thing->type == MT_ORBINAUT || thing->type == MT_JAWZ || thing->type == MT_GACHABOM
|| thing->type == MT_BANANA || thing->type == MT_EGGMANITEM || thing->type == MT_BALLHOG
|| thing->type == MT_SSMINE || thing->type == MT_LANDMINE || thing->type == MT_SINK
|| thing->type == MT_GARDENTOP
|| thing->type == MT_DROPTARGET
|| thing->type == MT_KART_LEFTOVER
|| (thing->type == MT_PLAYER && tm.thing->target != thing)))
else if (thing->type != MT_PLAYER && tm.thing->player && K_PlayerGuard(tm.thing->player) && P_BubbleCanReflect(tm.thing, thing))
{
// see if it went over / under
if (tm.thing->z > thing->z + thing->height)
@ -1031,6 +1025,52 @@ static BlockItReturn_t PIT_CheckThing(mobj_t *thing)
return K_BubbleShieldCollide(tm.thing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
}
// Bubble Shield reflect
if ((thing->type == MT_BUBBLESHIELD && thing->target->player && thing->target->player->bubbleblowup)
|| (thing->player && thing->player->bubbleblowup))
{
// see if it went over / under
if (tm.thing->z > thing->z + thing->height)
return BMIT_CONTINUE; // overhead
if (tm.thing->z + tm.thing->height < thing->z)
return BMIT_CONTINUE; // underneath
if (P_BubbleCanReflect(thing, tm.thing))
{
// don't let player hitbox touch it too
if (thing->player)
return BMIT_CONTINUE;
return K_BubbleShieldCollide(thing, tm.thing) ? BMIT_CONTINUE : BMIT_ABORT;
}
else if ((tm.thing->flags & MF_SHOOTABLE) && !thing->player)
{
P_DamageMobj(tm.thing, thing, thing->target, 1, DMG_NORMAL);
return BMIT_CONTINUE;
}
}
else if ((tm.thing->type == MT_BUBBLESHIELD && tm.thing->target->player && tm.thing->target->player->bubbleblowup)
|| (tm.thing->player && tm.thing->player->bubbleblowup))
{
// see if it went over / under
if (tm.thing->z > thing->z + thing->height)
return BMIT_CONTINUE; // overhead
if (tm.thing->z + tm.thing->height < thing->z)
return BMIT_CONTINUE; // underneath
if (P_BubbleCanReflect(tm.thing, thing))
{
// don't let player hitbox touch it too
if (tm.thing->player)
return BMIT_CONTINUE;
return K_BubbleShieldCollide(tm.thing, thing) ? BMIT_CONTINUE : BMIT_ABORT;
}
else if ((thing->flags & MF_SHOOTABLE) && !tm.thing->player)
{
P_DamageMobj(thing, tm.thing, tm.thing->target, 1, DMG_NORMAL);
return BMIT_CONTINUE;
}
}
// double make sure bubbles won't collide with anything else
if (thing->type == MT_BUBBLESHIELD || tm.thing->type == MT_BUBBLESHIELD)
return BMIT_CONTINUE;

View file

@ -5317,6 +5317,39 @@ cont:
// Kartitem stuff.
// These are held/thrown by players.
boolean P_IsKartItem(INT32 type)
{
switch (type)
{
case MT_POGOSPRING:
case MT_EGGMANITEM:
case MT_EGGMANITEM_SHIELD:
case MT_BANANA:
case MT_BANANA_SHIELD:
case MT_ORBINAUT:
case MT_ORBINAUT_SHIELD:
case MT_JAWZ:
case MT_JAWZ_SHIELD:
case MT_SSMINE:
case MT_SSMINE_SHIELD:
case MT_LANDMINE:
case MT_DROPTARGET:
case MT_DROPTARGET_SHIELD:
case MT_BALLHOG:
case MT_SPB:
case MT_BUBBLESHIELDTRAP:
case MT_GARDENTOP:
case MT_HYUDORO:
case MT_SINK:
case MT_GACHABOM:
return true;
default:
return false;
}
}
// This item is never attached to a player -- it can DIE
// unconditionally in death sectors.
boolean P_IsKartFieldItem(INT32 type)
@ -12939,19 +12972,20 @@ void P_SpawnPlayer(INT32 playernum)
if (G_IsPartyLocal(playernum))
{
// Spectating always enables director cam. If there
// is no one to view, this will do nothing. If
// someone enters the game later, it will
// automatically switch to that player.
K_ToggleDirector(G_PartyPosition(playernum), p->spectator);
// Spectators can switch to freecam. This should be
// disabled when they enter the race, or when the level
// changes.
if (!demo.playback)
{
camera[G_PartyPosition(playernum)].freecam = false;
displayplayers[G_PartyPosition(playernum)] = playernum;
}
// Spectating always enables director cam. If there
// is no one to view, this will do nothing. If
// someone enters the game later, it will
// automatically switch to that player.
K_ToggleDirector(G_PartyPosition(playernum), p->spectator);
}
else if (pcount == 1 && !p->spectator)
{

View file

@ -540,6 +540,7 @@ void P_InitCachedActions(void);
void P_RunCachedActions(void);
void P_AddCachedAction(mobj_t *mobj, INT32 statenum);
boolean P_IsKartItem(INT32 type);
boolean P_IsKartFieldItem(INT32 type);
boolean K_IsMissileOrKartItem(mobj_t *mo);
boolean P_CanDeleteKartItem(INT32 type);

View file

@ -457,6 +457,7 @@ static void P_ClearSingleMapHeaderInfo(INT16 num)
mapheaderinfo[num]->levelselect = 0;
mapheaderinfo[num]->levelflags = 0;
mapheaderinfo[num]->menuflags = 0;
mapheaderinfo[num]->playerLimit = MAXPLAYERS;
mapheaderinfo[num]->mobj_scale = FRACUNIT;
mapheaderinfo[num]->default_waypoint_radius = 0;
P_ClearMapHeaderLighting(&mapheaderinfo[num]->lighting);