Implement Follower Audience object

- Replaces Chao audience entirely
    - Convenient, because one of the two default follower types used in the audience is Chao
- Can provide one follower, or a list of followers, on the stringarg1 (seperated by spaces/commas) and it'll pick randomly between them
    - If not provided, uses the mapheader follower list
- Can provide one skincolor, or a list of skincolors, on the stringarg2 (seperated by spaces/commas) and it'll pick randomly between them
    - If not provided, uses the follower's default color
    - If the follower's default color is Match/Opposite or the user provides "Random" in stringarg2, pick a random skincolor
- If arg3 is set, floats in the air
    - MTF_OBJECTSPECIAL in binary format
- If arg4 is set, faces the closest player
    - MTF_AMBUSH in binary format
    - Uses some funny mathematical tricks to avoid checking on the same frame as every other audience member at once
This commit is contained in:
toaster 2023-04-20 23:10:59 +01:00
parent 5f15736626
commit a74b7995c9
9 changed files with 295 additions and 94 deletions

View file

@ -3860,14 +3860,6 @@ const char *const STATE_LIST[] = { // array length left dynamic for sanity testi
"S_DEZLASER_TRAIL4",
"S_DEZLASER_TRAIL5",
// Audience Members
"S_RANDOMAUDIENCE",
"S_AUDIENCE_CHAO_CHEER1",
"S_AUDIENCE_CHAO_CHEER2",
"S_AUDIENCE_CHAO_WIN1",
"S_AUDIENCE_CHAO_WIN2",
"S_AUDIENCE_CHAO_LOSE",
// 1.0 Kart Decoratives
"S_FLAYM1",
"S_FLAYM2",

View file

@ -4483,16 +4483,6 @@ state_t states[NUMSTATES] =
{SPR_DEZL, FF_FULLBRIGHT|2, 2, {NULL}, 0, 0, S_DEZLASER_TRAIL5}, // S_DEZLASER_TRAIL4
{SPR_DEZL, FF_FULLBRIGHT|1, 2, {NULL}, 0, 0, S_NULL}, // S_DEZLASER_TRAIL5
{SPR_NULL, 0, 1, {A_RandomStateRange}, S_AUDIENCE_CHAO_CHEER1, S_AUDIENCE_CHAO_CHEER2, S_RANDOMAUDIENCE}, // S_RANDOMAUDIENCE
{SPR_AUDI, 0, 5, {NULL}, 0, 0, S_AUDIENCE_CHAO_CHEER2}, // S_AUDIENCE_CHAO_CHEER1
{SPR_AUDI, 1, 20, {A_BunnyHop}, 7, 0, S_AUDIENCE_CHAO_CHEER1}, // S_AUDIENCE_CHAO_CHEER2
{SPR_AUDI, 2, 5, {NULL}, 0, 0, S_AUDIENCE_CHAO_WIN2}, // S_AUDIENCE_CHAO_WIN1
{SPR_AUDI, 3, 25, {A_BunnyHop}, 10, 0, S_AUDIENCE_CHAO_WIN1}, // S_AUDIENCE_CHAO_WIN2
{SPR_AUDI, 4|FF_ANIMATE, -1, {NULL}, 1, 17, S_NULL}, // S_AUDIENCE_CHAO_LOSE
{SPR_FLAM, FF_FULLBRIGHT|FF_ADD|0, 3, {NULL}, 0, 0, S_FLAYM2}, // S_FLAYM1,
{SPR_FLAM, FF_FULLBRIGHT|FF_ADD|1, 3, {NULL}, 0, 0, S_FLAYM3}, // S_FLAYM2,
{SPR_FLAM, FF_FULLBRIGHT|FF_ADD|2, 3, {NULL}, 0, 0, S_FLAYM4}, // S_FLAYM3,
@ -24880,7 +24870,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
{ // MT_RANDOMAUDIENCE
1488, // doomednum
S_RANDOMAUDIENCE, // spawnstate
S_UNKNOWN, // spawnstate
1000, // spawnhealth
S_NULL, // seestate
sfx_None, // seesound

View file

@ -4929,14 +4929,6 @@ typedef enum state
S_DEZLASER_TRAIL4,
S_DEZLASER_TRAIL5,
// Audience Members
S_RANDOMAUDIENCE,
S_AUDIENCE_CHAO_CHEER1,
S_AUDIENCE_CHAO_CHEER2,
S_AUDIENCE_CHAO_WIN1,
S_AUDIENCE_CHAO_WIN2,
S_AUDIENCE_CHAO_LOSE,
// 1.0 Kart Decoratives
S_FLAYM1,
S_FLAYM2,

View file

@ -114,6 +114,10 @@ void Obj_PlayerUsedRingShooter(mobj_t *base, player_t *player);
void Obj_RingShooterDelete(mobj_t *mo);
void Obj_UpdateRingShooterFace(mobj_t *part);
/* Follower Audience */
void Obj_RandomAudienceInit(mobj_t * mobj, mapthing_t *mthing);
void Obj_RandomAudienceThink(mobj_t * mobj);
#ifdef __cplusplus
} // extern "C"
#endif

View file

@ -53,6 +53,8 @@ typedef enum
PR_VOICES, // Player voice sounds
PR_RANDOMSKIN, // Random skin select from Heavy Magician(?)
PR_RANDOMAUDIENCE, // Audience randomisation
PR_RULESCRAMBLE, // Rule scrambing events
PR_MUSICSELECT, // Randomized music selection

View file

@ -15,4 +15,5 @@ target_sources(SRB2SDL2 PRIVATE
loops.c
drop-target.c
ring-shooter.c
audience.c
)

272
src/objects/audience.c Normal file
View file

@ -0,0 +1,272 @@
#include "../doomdef.h"
#include "../info.h"
#include "../g_game.h"
#include "../k_objects.h"
#include "../k_follower.h"
#include "../m_random.h"
#include "../p_local.h"
#include "../p_setup.h"
#include "../r_draw.h" // R_GetColorByName
#include "../r_main.h" // R_PointToAngle2, R_PointToDist2
#include "../z_zone.h" // Z_StrDup/Z_Free
#define audience_mainstate(o) ((o)->cvmem)
#define audience_bobamp(o) ((o)->cusval)
#define audience_bobspeed(o) ((o)->reactiontime)
#define audience_animoffset(o) ((o)->threshold)
#define audience_focusplayer(o) ((o)->lastlook)
#define audience_focusdelay(o) ((o)->movecount)
void
Obj_RandomAudienceInit
( mobj_t * mobj,
mapthing_t *mthing)
{
UINT16 *reflist = NULL;
UINT16 tempreflist[MAXHEADERFOLLOWERS];
UINT8 numref = 0;
INT32 followerpick = 0;
P_SetScale(mobj, (mobj->destscale <<= 1));
audience_mainstate(mobj) = S_NULL;
// Pick follower
{
if (mthing->stringargs[0] != NULL)
{
// From mapthing
char *stringcopy = Z_StrDup(mthing->stringargs[0]);
char *tok = strtok(stringcopy, " ,");
numref = 0;
while (tok && numref < MAXHEADERFOLLOWERS)
{
tempreflist[numref++] = K_FollowerAvailable(tok);
tok = strtok(NULL, " ,");
}
Z_Free(stringcopy);
reflist = tempreflist;
}
else
{
// From mapheader
if (!mapheaderinfo[gamemap-1])
{
// No mapheader, no shoes, no service.
return;
}
numref = mapheaderinfo[gamemap-1]->numFollowers;
reflist = mapheaderinfo[gamemap-1]->followers;
}
if (!numref || !reflist)
{
// This is the one thing a user should definitely be told about.
CONS_Alert(CONS_WARNING, "Mapthing %s: Follower audience has no valid followers to pick from!\n", sizeu1(mthing-mapthings));
return;
}
followerpick = reflist[P_RandomKey(PR_RANDOMAUDIENCE, numref)];
if (followerpick < 0 || followerpick >= numfollowers)
{
// Is this user error or user choice..?
P_RemoveMobj(mobj);
return;
}
}
// Handle storing follower properties on the object
{
audience_mainstate(mobj) = followers[followerpick].followstate;
P_SetMobjState(mobj, audience_mainstate(mobj));
if (P_MobjWasRemoved(mobj))
return;
if (mthing->args[2] != 0)
{
mobj->flags |= MF_NOGRAVITY;
}
if (mthing->args[3] != 0)
{
mobj->flags2 |= MF2_AMBUSH;
}
// The following is derived from the default bobamp
if (!(mobj->flags & MF_NOGRAVITY) && followers[followerpick].bobamp < 4*FRACUNIT)
{
audience_bobamp(mobj) = 4*mobj->scale;
}
else
{
audience_bobamp(mobj) = FixedMul(mobj->scale, followers[followerpick].bobamp);
}
audience_bobspeed(mobj) = followers[followerpick].bobspeed;
audience_focusplayer(mobj) = MAXPLAYERS;
if (P_RandomChance(PR_RANDOMAUDIENCE, FRACUNIT/2))
{
audience_animoffset(mobj) = 5;
}
}
// Handle colors
{
UINT16 colorpick = SKINCOLOR_NONE;
if (mthing->stringargs[1] != NULL)
{
if (!strcmp("Random", mthing->stringargs[1]))
{
colorpick = FOLLOWERCOLOR_MATCH;
}
else
{
char *stringcopy = Z_StrDup(mthing->stringargs[1]);
char *tok = strtok(stringcopy, " ");
numref = 0;
while (tok && numref < MAXHEADERFOLLOWERS)
{
tempreflist[numref++] = R_GetColorByName(tok);
tok = strtok(NULL, " ");
}
Z_Free(stringcopy);
if (numref)
{
colorpick = tempreflist[P_RandomKey(PR_RANDOMAUDIENCE, numref)];
}
}
}
if (colorpick == SKINCOLOR_NONE)
{
colorpick = followers[followerpick].defaultcolor;
}
if (colorpick == FOLLOWERCOLOR_MATCH
|| colorpick == FOLLOWERCOLOR_OPPOSITE)
{
colorpick = P_RandomKey(PR_RANDOMAUDIENCE, numskincolors-1)+1;
}
mobj->color = colorpick;
}
}
void
Obj_RandomAudienceThink
( mobj_t * mobj)
{
if (audience_mainstate(mobj) == S_NULL)
{
// Uninitialised, don't do anything funny.
return;
}
if (mobj->flags2 & MF2_AMBUSH)
{
if (audience_focusdelay(mobj) == 0)
{
fixed_t bestdist = INT32_MAX, dist;
UINT8 i;
audience_focusplayer(mobj) = MAXPLAYERS;
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] == false
|| players[i].spectator == true
|| P_MobjWasRemoved(players[i].mo))
continue;
dist = R_PointToDist2(
mobj->x,
mobj->y,
players[i].mo->x,
players[i].mo->y
);
if (dist >= bestdist)
continue;
dist = R_PointToDist2(
mobj->z,
0,
players[i].mo->z,
dist
);
if (dist >= bestdist)
continue;
bestdist = dist;
audience_focusplayer(mobj) = i;
}
// Try to add some spacing out so the object isn't constantly looking for players
audience_focusdelay(mobj) = TICRATE + min((bestdist/FRACUNIT), (2*TICRATE)) + (bestdist % TICRATE);
}
else
{
audience_focusdelay(mobj)--;
}
if (audience_focusplayer(mobj) < MAXPLAYERS && audience_focusplayer(mobj) >= 0)
{
mobj->angle = R_PointToAngle2(
mobj->x,
mobj->y,
players[audience_focusplayer(mobj)].mo->x,
players[audience_focusplayer(mobj)].mo->y
);
}
}
if (mobj->flags & MF_NOGRAVITY)
{
// This horrible calculation was inherited from k_follower.c, with only newlines (and a FRACUNIT offset) added
mobj->sprzoff = FixedMul(audience_bobamp(mobj),
FRACUNIT + FINESINE(((
FixedMul(4 * M_TAU_FIXED, audience_bobspeed(mobj))
* (leveltime + audience_animoffset(mobj))
) >> ANGLETOFINESHIFT) & FINEMASK));
// Gravity
if (mobj->flags2 & MF2_OBJECTFLIP)
{
mobj->sprzoff = -mobj->sprzoff;
}
}
else if (audience_animoffset(mobj) > 0)
{
// Skipped frames at spawn for offset in jumping
audience_animoffset(mobj)--;
}
else if (mobj->flags2 & MF2_OBJECTFLIP)
{
if (mobj->z + mobj->height >= mobj->ceilingz)
{
mobj->momz = -audience_bobamp(mobj);
P_SetMobjState(mobj, audience_mainstate(mobj));
}
}
else if (mobj->z <= mobj->floorz)
{
mobj->momz = audience_bobamp(mobj);
P_SetMobjState(mobj, audience_mainstate(mobj));
}
}

View file

@ -10267,39 +10267,9 @@ void P_SceneryThinker(mobj_t *mobj)
mobj->renderflags &= ~RF_DONTDRAW;
}
if (mobj->type != MT_RANDOMAUDIENCE)
return;
if (mobj->type == MT_RANDOMAUDIENCE)
{
if (!mobj->colorized) // a fan of someone?
return;
if (mobj->threshold >= 0) // not already happy or sad?
{
if (!playeringame[mobj->threshold] || players[mobj->threshold].spectator) // focused on a valid player?
return;
if (!(players[mobj->threshold].exiting) && !(players[mobj->threshold].pflags & PF_NOCONTEST)) // not finished yet?
return;
if (K_IsPlayerLosing(&players[mobj->threshold]))
mobj->threshold = -2;
else
{
mobj->threshold = -1;
S_StartSound(mobj, sfx_chaooo);
}
}
if (mobj->threshold == -1)
mobj->angle += ANGLE_22h;
if (((statenum_t)(mobj->state-states) != S_AUDIENCE_CHAO_CHEER2) || (mobj->tics != states[S_AUDIENCE_CHAO_CHEER2].tics)) // not at the start of your cheer jump?
return;
mobj->momz = 0;
P_SetMobjState(mobj, ((mobj->threshold == -1) ? S_AUDIENCE_CHAO_WIN2 : S_AUDIENCE_CHAO_LOSE));
Obj_RandomAudienceThink(mobj);
}
}
@ -10814,41 +10784,6 @@ mobj_t *P_SpawnMobj(fixed_t x, fixed_t y, fixed_t z, mobjtype_t type)
case MT_CDUFO:
P_SetScale(mobj, (mobj->destscale = 3*FRACUNIT/2));
break;
case MT_RANDOMAUDIENCE:
{
fixed_t randu = P_RandomFixed(PR_UNDEFINED);
P_SetScale(mobj, (mobj->destscale <<= 1));
if (randu < (FRACUNIT/9)) // a fan of someone?
{
UINT8 i, pcount = 0;
UINT8 pnum[MAXPLAYERS];
for (i = 0; i < MAXPLAYERS; i++)
{
if (!playeringame[i])
continue;
pnum[pcount] = i;
pcount++;
}
if (pcount)
{
mobj->threshold = pnum[P_RandomKey(PR_UNDEFINED, pcount)];
mobj->color = players[mobj->threshold].skincolor;
mobj->colorized = true;
break;
}
}
if (randu > (FRACUNIT/2))
{
mobj->color = P_RandomKey(PR_UNDEFINED, numskincolors-1)+1;
break;
}
mobj->color = SKINCOLOR_CYAN;
break;
}
case MT_MARBLETORCH:
P_SpawnMobj(mobj->x, mobj->y, mobj->z + (29*mobj->scale), MT_MARBLELIGHT);
break;
@ -13217,6 +13152,15 @@ static boolean P_SetupSpawnedMapThing(mapthing_t *mthing, mobj_t *mobj, boolean
}
break;
}
case MT_RANDOMAUDIENCE:
{
Obj_RandomAudienceInit(mobj, mthing);
if (P_MobjWasRemoved(mobj))
return false;
break;
}
case MT_AAZTREE_HELPER:
{
fixed_t top = mobj->z;

View file

@ -6739,6 +6739,10 @@ static void P_ConvertBinaryThingTypes(void)
case 1305: //Rollout Rock
mapthings[i].args[0] = !!(mapthings[i].options & MTF_AMBUSH);
break;
case 1488: // Follower Audience (unfortunately numbered)
mapthings[i].args[2] = !!(mapthings[i].options & MTF_OBJECTSPECIAL);
mapthings[i].args[3] = !!(mapthings[i].options & MTF_AMBUSH);
break;
case 1500: //Glaregoyle
case 1501: //Glaregoyle (Up)
case 1502: //Glaregoyle (Down)