quake2-rerelease-dll/rerelease/p_hud.cpp
2023-10-03 14:43:06 -04:00

1134 lines
29 KiB
C++

// Copyright (c) ZeniMax Media Inc.
// Licensed under the GNU General Public License 2.0.
#include "g_local.h"
#include "g_statusbar.h"
/*
======================================================================
INTERMISSION
======================================================================
*/
void DeathmatchScoreboard(edict_t *ent);
void MoveClientToIntermission(edict_t *ent)
{
// [Paril-KEX]
if (ent->client->ps.pmove.pm_type != PM_FREEZE)
ent->s.event = EV_OTHER_TELEPORT;
if (deathmatch->integer)
ent->client->showscores = true;
ent->s.origin = level.intermission_origin;
ent->client->ps.pmove.origin = level.intermission_origin;
ent->client->ps.viewangles = level.intermission_angle;
ent->client->ps.pmove.pm_type = PM_FREEZE;
ent->client->ps.gunindex = 0;
ent->client->ps.gunskin = 0;
ent->client->ps.damage_blend[3] = ent->client->ps.screen_blend[3] = 0;
ent->client->ps.rdflags = RDF_NONE;
// clean up powerup info
ent->client->quad_time = 0_ms;
ent->client->invincible_time = 0_ms;
ent->client->breather_time = 0_ms;
ent->client->enviro_time = 0_ms;
ent->client->invisible_time = 0_ms;
ent->client->grenade_blew_up = false;
ent->client->grenade_time = 0_ms;
ent->client->showhelp = false;
ent->client->showscores = false;
globals.server_flags &= ~SERVER_FLAG_SLOW_TIME;
// RAFAEL
ent->client->quadfire_time = 0_ms;
// RAFAEL
// ROGUE
ent->client->ir_time = 0_ms;
ent->client->nuke_time = 0_ms;
ent->client->double_time = 0_ms;
ent->client->tracker_pain_time = 0_ms;
// ROGUE
ent->viewheight = 0;
ent->s.modelindex = 0;
ent->s.modelindex2 = 0;
ent->s.modelindex3 = 0;
ent->s.modelindex = 0;
ent->s.effects = EF_NONE;
ent->s.sound = 0;
ent->solid = SOLID_NOT;
ent->movetype = MOVETYPE_NOCLIP;
gi.linkentity(ent);
// add the layout
if (deathmatch->integer)
{
DeathmatchScoreboard(ent);
ent->client->showscores = true;
}
}
// [Paril-KEX] update the level entry for end-of-unit screen
void G_UpdateLevelEntry()
{
if (!level.entry)
return;
level.entry->found_secrets = level.found_secrets;
level.entry->total_secrets = level.total_secrets;
level.entry->killed_monsters = level.killed_monsters;
level.entry->total_monsters = level.total_monsters;
}
inline void G_EndOfUnitEntry(std::stringstream &layout, const int &y, const level_entry_t &entry)
{
layout << G_Fmt("yv {} ", y);
// we didn't visit this level, so print it as an unknown entry
if (!*entry.pretty_name)
{
layout << "table_row 1 ??? ";
return;
}
layout << G_Fmt("table_row 4 \"{}\" ", entry.pretty_name) <<
G_Fmt("{}/{} ", entry.killed_monsters, entry.total_monsters) <<
G_Fmt("{}/{} ", entry.found_secrets, entry.total_secrets);
int32_t minutes = entry.time.milliseconds() / 60000;
int32_t seconds = (entry.time.milliseconds() / 1000) % 60;
int32_t milliseconds = entry.time.milliseconds() % 1000;
layout << G_Fmt("{:02}:{:02}:{:03} ", minutes, seconds, milliseconds);
}
void G_EndOfUnitMessage()
{
// [Paril-KEX] update game level entry
G_UpdateLevelEntry();
std::stringstream layout;
// sort entries
std::sort(game.level_entries.begin(), game.level_entries.end(), [](const level_entry_t &a, const level_entry_t &b) {
int32_t a_order = a.visit_order ? a.visit_order : (*a.pretty_name ? (MAX_LEVELS_PER_UNIT + 1) : (MAX_LEVELS_PER_UNIT + 2));
int32_t b_order = b.visit_order ? b.visit_order : (*b.pretty_name ? (MAX_LEVELS_PER_UNIT + 1) : (MAX_LEVELS_PER_UNIT + 2));
return a_order < b_order;
});
layout << "start_table 4 $m_eou_level $m_eou_kills $m_eou_secrets $m_eou_time ";
int y = 16;
level_entry_t totals {};
int32_t num_rows = 0;
for (auto &entry : game.level_entries)
{
if (!*entry.map_name)
break;
G_EndOfUnitEntry(layout, y, entry);
y += 8;
totals.found_secrets += entry.found_secrets;
totals.killed_monsters += entry.killed_monsters;
totals.time += entry.time;
totals.total_monsters += entry.total_monsters;
totals.total_secrets += entry.total_secrets;
if (entry.visit_order)
num_rows++;
}
y += 8;
// make this a space so it prints totals
if (num_rows > 1)
{
layout << "table_row 0 "; // empty row to separate totals
totals.pretty_name[0] = ' ';
G_EndOfUnitEntry(layout, y, totals);
}
layout << "xv 160 yt 0 draw_table ";
layout << "ifgef " << (level.intermission_server_frame + (5_sec).frames()) << " yb -48 xv 0 loc_cstring2 0 \"$m_eou_press_button\" endif ";
gi.WriteByte(svc_layout);
gi.WriteString(layout.str().c_str());
gi.multicast(vec3_origin, MULTICAST_ALL, true);
for (auto player : active_players())
player->client->showeou = true;
}
// data is binary now.
// u8 num_teams
// u8 num_players
// [ repeat num_teams:
// string team_name
// ]
// [ repeat num_players:
// u8 client_index
// s32 score
// u8 ranking
// (if num_teams > 0)
// u8 team
// ]
void G_ReportMatchDetails(bool is_end)
{
static std::array<uint32_t, MAX_CLIENTS> player_ranks;
player_ranks = {};
// CTF/TDM is simple
if (ctf->integer || teamplay->integer)
{
CTFCalcRankings(player_ranks);
gi.WriteByte(2);
gi.WriteString("RED TEAM"); // team 0
gi.WriteString("BLUE TEAM"); // team 1
}
else
{
// sort players by score, then match everybody to
// the current highest score downwards until we run out of players.
static std::array<edict_t *, MAX_CLIENTS> sorted_players;
size_t num_active_players = 0;
for (auto player : active_players())
sorted_players[num_active_players++] = player;
std::sort(sorted_players.begin(), sorted_players.begin() + num_active_players, [](const edict_t *a, const edict_t *b) { return b->client->resp.score < a->client->resp.score; });
int32_t current_score = INT_MIN;
int32_t current_rank = 0;
for (size_t i = 0; i < num_active_players; i++)
{
if (!current_rank || sorted_players[i]->client->resp.score != current_score)
{
current_rank++;
current_score = sorted_players[i]->client->resp.score;
}
player_ranks[sorted_players[i]->s.number - 1] = current_rank;
}
gi.WriteByte(0);
}
uint8_t num_players = 0;
for (auto player : active_players())
{
// leave spectators out of this data, they don't need to be seen.
if (player->client->pers.spawned && !player->client->resp.spectator)
{
// just in case...
if (G_TeamplayEnabled() && player->client->resp.ctf_team == CTF_NOTEAM)
continue;
num_players++;
}
}
gi.WriteByte(num_players);
for (auto player : active_players())
{
// leave spectators out of this data, they don't need to be seen.
if (player->client->pers.spawned && !player->client->resp.spectator)
{
// just in case...
if (G_TeamplayEnabled() && player->client->resp.ctf_team == CTF_NOTEAM)
continue;
gi.WriteByte(player->s.number - 1);
gi.WriteLong(player->client->resp.score);
gi.WriteByte(player_ranks[player->s.number - 1]);
if (G_TeamplayEnabled())
gi.WriteByte(player->client->resp.ctf_team == CTF_TEAM1 ? 0 : 1);
}
}
gi.ReportMatchDetails_Multicast(is_end);
}
void BeginIntermission(edict_t *targ)
{
edict_t *ent, *client;
if (level.intermissiontime)
return; // already activated
// ZOID
if (ctf->integer)
CTFCalcScores();
// ZOID
game.autosaved = false;
level.intermissiontime = level.time;
// respawn any dead clients
for (uint32_t i = 0; i < game.maxclients; i++)
{
client = g_edicts + 1 + i;
if (!client->inuse)
continue;
if (client->health <= 0)
{
// give us our max health back since it will reset
// to pers.health; in instanced items we'd lose the items
// we touched so we always want to respawn with our max.
if (P_UseCoopInstancedItems())
client->client->pers.health = client->client->pers.max_health = client->max_health;
respawn(client);
}
}
level.intermission_server_frame = gi.ServerFrame();
level.changemap = targ->map;
level.intermission_clear = targ->spawnflags.has(SPAWNFLAG_CHANGELEVEL_CLEAR_INVENTORY);
level.intermission_eou = false;
level.intermission_fade = targ->spawnflags.has(SPAWNFLAG_CHANGELEVEL_FADE_OUT);
// destroy all player trails
PlayerTrail_Destroy(nullptr);
// [Paril-KEX] update game level entry
G_UpdateLevelEntry();
if (strstr(level.changemap, "*"))
{
if (coop->integer)
{
for (uint32_t i = 0; i < game.maxclients; i++)
{
client = g_edicts + 1 + i;
if (!client->inuse)
continue;
// strip players of all keys between units
for (uint32_t n = 0; n < IT_TOTAL; n++)
if (itemlist[n].flags & IF_KEY)
client->client->pers.inventory[n] = 0;
}
}
if (level.achievement && level.achievement[0])
{
gi.WriteByte(svc_achievement);
gi.WriteString(level.achievement);
gi.multicast(vec3_origin, MULTICAST_ALL, true);
}
level.intermission_eou = true;
// "no end of unit" maps handle intermission differently
if (!targ->spawnflags.has(SPAWNFLAG_CHANGELEVEL_NO_END_OF_UNIT))
G_EndOfUnitMessage();
else if (targ->spawnflags.has(SPAWNFLAG_CHANGELEVEL_IMMEDIATE_LEAVE) && !deathmatch->integer)
{
// Need to call this now
G_ReportMatchDetails(true);
level.exitintermission = 1; // go immediately to the next level
return;
}
}
else
{
if (!deathmatch->integer)
{
level.exitintermission = 1; // go immediately to the next level
return;
}
}
// Call while intermission is running
G_ReportMatchDetails(true);
level.exitintermission = 0;
if (!level.level_intermission_set)
{
// find an intermission spot
ent = G_FindByString<&edict_t::classname>(nullptr, "info_player_intermission");
if (!ent)
{ // the map creator forgot to put in an intermission point...
ent = G_FindByString<&edict_t::classname>(nullptr, "info_player_start");
if (!ent)
ent = G_FindByString<&edict_t::classname>(nullptr, "info_player_deathmatch");
}
else
{ // choose one of four spots
int32_t i = irandom(4);
while (i--)
{
ent = G_FindByString<&edict_t::classname>(ent, "info_player_intermission");
if (!ent) // wrap around the list
ent = G_FindByString<&edict_t::classname>(ent, "info_player_intermission");
}
}
level.intermission_origin = ent->s.origin;
level.intermission_angle = ent->s.angles;
}
// move all clients to the intermission point
for (uint32_t i = 0; i < game.maxclients; i++)
{
client = g_edicts + 1 + i;
if (!client->inuse)
continue;
MoveClientToIntermission(client);
}
}
constexpr size_t MAX_SCOREBOARD_SIZE = 1024;
/*
==================
DeathmatchScoreboardMessage
==================
*/
void DeathmatchScoreboardMessage(edict_t *ent, edict_t *killer)
{
static std::string entry, string;
size_t j;
int sorted[MAX_CLIENTS];
int sortedscores[MAX_CLIENTS];
int score;
int x, y;
gclient_t *cl;
edict_t *cl_ent;
const char *tag;
// ZOID
if (G_TeamplayEnabled())
{
CTFScoreboardMessage(ent, killer);
return;
}
// ZOID
entry.clear();
string.clear();
// sort the clients by score
uint32_t total = 0;
for (uint32_t i = 0; i < game.maxclients; i++)
{
cl_ent = g_edicts + 1 + i;
if (!cl_ent->inuse || game.clients[i].resp.spectator)
continue;
score = game.clients[i].resp.score;
for (j = 0; j < total; j++)
{
if (score > sortedscores[j])
break;
}
for (uint32_t k = total; k > j; k--)
{
sorted[k] = sorted[k - 1];
sortedscores[k] = sortedscores[k - 1];
}
sorted[j] = i;
sortedscores[j] = score;
total++;
}
// add the clients in sorted order
if (total > 16)
total = 16;
for (uint32_t i = 0; i < total; i++)
{
cl = &game.clients[sorted[i]];
cl_ent = g_edicts + 1 + sorted[i];
x = (i >= 8) ? 130 : -72;
y = 0 + 32 * (i % 8);
// add a dogtag
// [Paril-KEX] use dynamic dogtags
tag = nullptr;
//===============
// ROGUE
// allow new DM games to override the tag picture
if (gamerules->integer)
{
if (DMGame.DogTag)
DMGame.DogTag(cl_ent, killer, &tag);
}
// ROGUE
//===============
if (tag)
{
fmt::format_to(std::back_inserter(entry), FMT_STRING("xv {} yv {} picn {} "), x + 32, y, tag);
if (string.length() + entry.length() > MAX_SCOREBOARD_SIZE)
break;
string += entry;
}
else
{
fmt::format_to(std::back_inserter(entry), FMT_STRING("xv {} yv {} dogtag {} "), x + 32, y, sorted[i]);
if (string.length() + entry.length() > MAX_SCOREBOARD_SIZE)
break;
string += entry;
}
entry.clear();
fmt::format_to(std::back_inserter(entry),
FMT_STRING("client {} {} {} {} {} {} "),
x, y, sorted[i], cl->resp.score, cl->ping, (int32_t) (level.time - cl->resp.entertime).minutes());
if (string.length() + entry.length() > MAX_SCOREBOARD_SIZE)
break;
string += entry;
entry.clear();
}
// [Paril-KEX] time & frags
if (fraglimit->integer)
{
fmt::format_to(std::back_inserter(string), FMT_STRING("xv -20 yv -10 loc_string2 1 $g_score_frags \"{}\" "), fraglimit->integer);
}
if (timelimit->value && !level.intermissiontime)
{
fmt::format_to(std::back_inserter(string), FMT_STRING("xv 340 yv -10 time_limit {} "), gi.ServerFrame() + ((gtime_t::from_min(timelimit->value) - level.time)).milliseconds() / gi.frame_time_ms);
}
if (level.intermissiontime)
fmt::format_to(std::back_inserter(string), FMT_STRING("ifgef {} yb -48 xv 0 loc_cstring2 0 \"$m_eou_press_button\" endif "), (level.intermission_server_frame + (5_sec).frames()));
gi.WriteByte(svc_layout);
gi.WriteString(string.c_str());
}
/*
==================
DeathmatchScoreboard
Draw instead of help message.
Note that it isn't that hard to overflow the 1400 byte message limit!
==================
*/
void DeathmatchScoreboard(edict_t *ent)
{
DeathmatchScoreboardMessage(ent, ent->enemy);
gi.unicast(ent, true);
ent->client->menutime = level.time + 3_sec;
}
/*
==================
Cmd_Score_f
Display the scoreboard
==================
*/
void Cmd_Score_f(edict_t *ent)
{
if (level.intermissiontime)
return;
ent->client->showinventory = false;
ent->client->showhelp = false;
globals.server_flags &= ~SERVER_FLAG_SLOW_TIME;
// ZOID
if (ent->client->menu)
PMenu_Close(ent);
// ZOID
if (!deathmatch->integer && !coop->integer)
return;
if (ent->client->showscores)
{
ent->client->showscores = false;
ent->client->update_chase = true;
return;
}
ent->client->showscores = true;
DeathmatchScoreboard(ent);
}
/*
==================
HelpComputer
Draw help computer.
==================
*/
void HelpComputer(edict_t *ent)
{
const char *sk;
if (skill->integer == 0)
sk = "$m_easy";
else if (skill->integer == 1)
sk = "$m_medium";
else if (skill->integer == 2)
sk = "$m_hard";
else
sk = "$m_nightmare";
// send the layout
std::string helpString = "";
helpString += G_Fmt(
"xv 32 yv 8 picn help " // background
"xv 0 yv 25 cstring2 \"{}\" ", // level name
level.level_name);
if (level.is_n64)
{
helpString += G_Fmt("xv 0 yv 54 loc_cstring 1 \"{{}}\" \"{}\" ", // help 1
game.helpmessage1);
}
else
{
int y = 54;
if (strlen(game.helpmessage1))
{
helpString += G_Fmt("xv 0 yv {} loc_cstring2 0 \"$g_pc_primary_objective\" " // title
"xv 0 yv {} loc_cstring 0 \"{}\" ",
y,
y + 11,
game.helpmessage1);
y += 58;
}
if (strlen(game.helpmessage2))
{
helpString += G_Fmt("xv 0 yv {} loc_cstring2 0 \"$g_pc_secondary_objective\" " // title
"xv 0 yv {} loc_cstring 0 \"{}\" ",
y,
y + 11,
game.helpmessage2);
}
}
helpString += G_Fmt("xv 55 yv 164 loc_string2 0 \"{}\" "
"xv 265 yv 164 loc_rstring2 1 \"{{}}: {}/{}\" \"$g_pc_goals\" "
"xv 55 yv 172 loc_string2 1 \"{{}}: {}/{}\" \"$g_pc_kills\" "
"xv 265 yv 172 loc_rstring2 1 \"{{}}: {}/{}\" \"$g_pc_secrets\" ",
sk,
level.found_goals, level.total_goals,
level.killed_monsters, level.total_monsters,
level.found_secrets, level.total_secrets);
gi.WriteByte(svc_layout);
gi.WriteString(helpString.c_str());
gi.unicast(ent, true);
}
/*
==================
Cmd_Help_f
Display the current help message
==================
*/
void Cmd_Help_f(edict_t *ent)
{
// this is for backwards compatability
if (deathmatch->integer)
{
Cmd_Score_f(ent);
return;
}
if (level.intermissiontime)
return;
ent->client->showinventory = false;
ent->client->showscores = false;
if (ent->client->showhelp &&
(ent->client->pers.game_help1changed == game.help1changed ||
ent->client->pers.game_help2changed == game.help2changed))
{
ent->client->showhelp = false;
globals.server_flags &= ~SERVER_FLAG_SLOW_TIME;
return;
}
ent->client->showhelp = true;
ent->client->pers.helpchanged = 0;
globals.server_flags |= SERVER_FLAG_SLOW_TIME;
HelpComputer(ent);
}
//=======================================================================
// [Paril-KEX] for stats we want to always be set in coop
// even if we're spectating
void G_SetCoopStats(edict_t *ent)
{
if (coop->integer && g_coop_enable_lives->integer)
ent->client->ps.stats[STAT_LIVES] = ent->client->pers.lives + 1;
else
ent->client->ps.stats[STAT_LIVES] = 0;
// stat for text on what we're doing for respawn
if (ent->client->coop_respawn_state)
ent->client->ps.stats[STAT_COOP_RESPAWN] = CONFIG_COOP_RESPAWN_STRING + (ent->client->coop_respawn_state - COOP_RESPAWN_IN_COMBAT);
else
ent->client->ps.stats[STAT_COOP_RESPAWN] = 0;
}
struct powerup_info_t
{
item_id_t item;
gtime_t gclient_t::*time_ptr = nullptr;
int32_t gclient_t::*count_ptr = nullptr;
} powerup_table[] = {
{ IT_ITEM_QUAD, &gclient_t::quad_time },
{ IT_ITEM_QUADFIRE, &gclient_t::quadfire_time },
{ IT_ITEM_DOUBLE, &gclient_t::double_time },
{ IT_ITEM_INVULNERABILITY, &gclient_t::invincible_time },
{ IT_ITEM_INVISIBILITY, &gclient_t::invisible_time },
{ IT_ITEM_ENVIROSUIT, &gclient_t::enviro_time },
{ IT_ITEM_REBREATHER, &gclient_t::breather_time },
{ IT_ITEM_IR_GOGGLES, &gclient_t::ir_time },
{ IT_ITEM_SILENCER, nullptr, &gclient_t::silencer_shots }
};
/*
===============
G_SetStats
===============
*/
void G_SetStats(edict_t *ent)
{
gitem_t *item;
item_id_t index;
int cells = 0;
item_id_t power_armor_type;
unsigned int invIndex;
//
// health
//
if (ent->s.renderfx & RF_USE_DISGUISE)
ent->client->ps.stats[STAT_HEALTH_ICON] = level.disguise_icon;
else
ent->client->ps.stats[STAT_HEALTH_ICON] = level.pic_health;
ent->client->ps.stats[STAT_HEALTH] = ent->health;
//
// weapons
//
uint32_t weaponbits = 0;
for (invIndex = IT_WEAPON_GRAPPLE; invIndex <= IT_WEAPON_DISRUPTOR; invIndex++)
{
if (ent->client->pers.inventory[invIndex])
{
weaponbits |= 1 << GetItemByIndex((item_id_t) invIndex)->weapon_wheel_index;
}
}
ent->client->ps.stats[STAT_WEAPONS_OWNED_1] = (weaponbits & 0xFFFF);
ent->client->ps.stats[STAT_WEAPONS_OWNED_2] = (weaponbits >> 16);
ent->client->ps.stats[STAT_ACTIVE_WHEEL_WEAPON] = (ent->client->newweapon ? ent->client->newweapon->weapon_wheel_index :
ent->client->pers.weapon ? ent->client->pers.weapon->weapon_wheel_index :
-1);
ent->client->ps.stats[STAT_ACTIVE_WEAPON] = ent->client->pers.weapon ? ent->client->pers.weapon->weapon_wheel_index : -1;
//
// ammo
//
ent->client->ps.stats[STAT_AMMO_ICON] = 0;
ent->client->ps.stats[STAT_AMMO] = 0;
if (ent->client->pers.weapon && ent->client->pers.weapon->ammo)
{
item = GetItemByIndex(ent->client->pers.weapon->ammo);
if (!G_CheckInfiniteAmmo(item))
{
ent->client->ps.stats[STAT_AMMO_ICON] = gi.imageindex(item->icon);
ent->client->ps.stats[STAT_AMMO] = ent->client->pers.inventory[ent->client->pers.weapon->ammo];
}
}
memset(&ent->client->ps.stats[STAT_AMMO_INFO_START], 0, sizeof(uint16_t) * NUM_AMMO_STATS);
for (unsigned int ammoIndex = AMMO_BULLETS; ammoIndex < AMMO_MAX; ++ammoIndex)
{
gitem_t *ammo = GetItemByAmmo((ammo_t) ammoIndex);
uint16_t val = G_CheckInfiniteAmmo(ammo) ? AMMO_VALUE_INFINITE : clamp(ent->client->pers.inventory[ammo->id], 0, AMMO_VALUE_INFINITE - 1);
G_SetAmmoStat((uint16_t *) &ent->client->ps.stats[STAT_AMMO_INFO_START], ammo->ammo_wheel_index, val);
}
//
// armor
//
power_armor_type = PowerArmorType(ent);
if (power_armor_type)
cells = ent->client->pers.inventory[IT_AMMO_CELLS];
index = ArmorIndex(ent);
if (power_armor_type && (!index || (level.time.milliseconds() % 3000) < 1500))
{ // flash between power armor and other armor icon
ent->client->ps.stats[STAT_ARMOR_ICON] = power_armor_type == IT_ITEM_POWER_SHIELD ? gi.imageindex("i_powershield") : gi.imageindex("i_powerscreen");
ent->client->ps.stats[STAT_ARMOR] = cells;
}
else if (index)
{
item = GetItemByIndex(index);
ent->client->ps.stats[STAT_ARMOR_ICON] = gi.imageindex(item->icon);
ent->client->ps.stats[STAT_ARMOR] = ent->client->pers.inventory[index];
}
else
{
ent->client->ps.stats[STAT_ARMOR_ICON] = 0;
ent->client->ps.stats[STAT_ARMOR] = 0;
}
//
// pickup message
//
if (level.time > ent->client->pickup_msg_time)
{
ent->client->ps.stats[STAT_PICKUP_ICON] = 0;
ent->client->ps.stats[STAT_PICKUP_STRING] = 0;
}
// owned powerups
memset(&ent->client->ps.stats[STAT_POWERUP_INFO_START], 0, sizeof(uint16_t) * NUM_POWERUP_STATS);
for (unsigned int powerupIndex = POWERUP_SCREEN; powerupIndex < POWERUP_MAX; ++powerupIndex)
{
gitem_t *powerup = GetItemByPowerup((powerup_t) powerupIndex);
uint16_t val;
switch (powerup->id)
{
case IT_ITEM_POWER_SCREEN:
case IT_ITEM_POWER_SHIELD:
if (!ent->client->pers.inventory[powerup->id])
val = 0;
else if (ent->flags & FL_POWER_ARMOR)
val = 2;
else
val = 1;
break;
case IT_ITEM_FLASHLIGHT:
if (!ent->client->pers.inventory[powerup->id])
val = 0;
else if (ent->flags & FL_FLASHLIGHT)
val = 2;
else
val = 1;
break;
default:
val = clamp(ent->client->pers.inventory[powerup->id], 0, 3);
break;
}
G_SetPowerupStat((uint16_t *) &ent->client->ps.stats[STAT_POWERUP_INFO_START], powerup->powerup_wheel_index, val);
}
ent->client->ps.stats[STAT_TIMER_ICON] = 0;
ent->client->ps.stats[STAT_TIMER] = 0;
//
// timers
//
// PGM
if (ent->client->owned_sphere)
{
if (ent->client->owned_sphere->spawnflags == SPHERE_DEFENDER) // defender
ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex("p_defender");
else if (ent->client->owned_sphere->spawnflags == SPHERE_HUNTER) // hunter
ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex("p_hunter");
else if (ent->client->owned_sphere->spawnflags == SPHERE_VENGEANCE) // vengeance
ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex("p_vengeance");
else // error case
ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex("i_fixme");
ent->client->ps.stats[STAT_TIMER] = ceil(ent->client->owned_sphere->wait - level.time.seconds());
}
else
{
powerup_info_t *best_powerup = nullptr;
for (auto &powerup : powerup_table)
{
auto *powerup_time = powerup.time_ptr ? &(ent->client->*powerup.time_ptr) : nullptr;
auto *powerup_count = powerup.count_ptr ? &(ent->client->*powerup.count_ptr) : nullptr;
if (powerup_time && *powerup_time <= level.time)
continue;
else if (powerup_count && !*powerup_count)
continue;
if (!best_powerup)
{
best_powerup = &powerup;
continue;
}
if (powerup_time && *powerup_time < ent->client->*best_powerup->time_ptr)
{
best_powerup = &powerup;
continue;
}
else if (powerup_count && !best_powerup->time_ptr)
{
best_powerup = &powerup;
continue;
}
}
if (best_powerup)
{
int16_t value;
if (best_powerup->count_ptr)
value = (ent->client->*best_powerup->count_ptr);
else
value = ceil((ent->client->*best_powerup->time_ptr - level.time).seconds());
ent->client->ps.stats[STAT_TIMER_ICON] = gi.imageindex(GetItemByIndex(best_powerup->item)->icon);
ent->client->ps.stats[STAT_TIMER] = value;
}
}
// PGM
//
// selected item
//
ent->client->ps.stats[STAT_SELECTED_ITEM] = ent->client->pers.selected_item;
if (ent->client->pers.selected_item == IT_NULL)
ent->client->ps.stats[STAT_SELECTED_ICON] = 0;
else
{
ent->client->ps.stats[STAT_SELECTED_ICON] = gi.imageindex(itemlist[ent->client->pers.selected_item].icon);
if (ent->client->pers.selected_item_time < level.time)
ent->client->ps.stats[STAT_SELECTED_ITEM_NAME] = 0;
}
//
// layouts
//
ent->client->ps.stats[STAT_LAYOUTS] = 0;
if (deathmatch->integer)
{
if (ent->client->pers.health <= 0 || level.intermissiontime || ent->client->showscores)
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_LAYOUT;
if (ent->client->showinventory && ent->client->pers.health > 0)
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_INVENTORY;
}
else
{
if (ent->client->showscores || ent->client->showhelp || ent->client->showeou)
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_LAYOUT;
if (ent->client->showinventory && ent->client->pers.health > 0)
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_INVENTORY;
if (ent->client->showhelp)
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_HELP;
}
if (level.intermissiontime || ent->client->awaiting_respawn)
{
if (ent->client->awaiting_respawn || (level.intermission_eou || level.is_n64 || (deathmatch->integer && level.intermissiontime)))
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_HIDE_HUD;
// N64 always merges into one screen on level ends
if (level.intermission_eou || level.is_n64 || (deathmatch->integer && level.intermissiontime))
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_INTERMISSION;
}
if (level.story_active)
ent->client->ps.stats[STAT_LAYOUTS] |= LAYOUTS_HIDE_CROSSHAIR;
else
ent->client->ps.stats[STAT_LAYOUTS] &= ~LAYOUTS_HIDE_CROSSHAIR;
// [Paril-KEX] key display
if (!deathmatch->integer)
{
int32_t key_offset = 0;
player_stat_t stat = STAT_KEY_A;
ent->client->ps.stats[STAT_KEY_A] =
ent->client->ps.stats[STAT_KEY_B] =
ent->client->ps.stats[STAT_KEY_C] = 0;
// there's probably a way to do this in one pass but
// I'm lazy
std::array<item_id_t, IT_TOTAL> keys_held;
size_t num_keys_held = 0;
for (auto &item : itemlist)
{
if (!(item.flags & IF_KEY))
continue;
else if (!ent->client->pers.inventory[item.id])
continue;
keys_held[num_keys_held++] = item.id;
}
if (num_keys_held > 3)
key_offset = (int32_t) (level.time.seconds() / 5);
for (int32_t i = 0; i < min(num_keys_held, (size_t) 3); i++, stat = (player_stat_t) (stat + 1))
ent->client->ps.stats[stat] = gi.imageindex(GetItemByIndex(keys_held[(i + key_offset) % num_keys_held])->icon);
}
//
// frags
//
ent->client->ps.stats[STAT_FRAGS] = ent->client->resp.score;
//
// help icon / current weapon if not shown
//
if (ent->client->pers.helpchanged >= 1 && ent->client->pers.helpchanged <= 2 && (level.time.milliseconds() % 1000) < 500) // haleyjd: time-limited
ent->client->ps.stats[STAT_HELPICON] = gi.imageindex("i_help");
else if ((ent->client->pers.hand == CENTER_HANDED) && ent->client->pers.weapon)
ent->client->ps.stats[STAT_HELPICON] = gi.imageindex(ent->client->pers.weapon->icon);
else
ent->client->ps.stats[STAT_HELPICON] = 0;
ent->client->ps.stats[STAT_SPECTATOR] = 0;
// set & run the health bar stuff
for (size_t i = 0; i < MAX_HEALTH_BARS; i++)
{
byte *health_byte = reinterpret_cast<byte *>(&ent->client->ps.stats[STAT_HEALTH_BARS]) + i;
if (!level.health_bar_entities[i])
*health_byte = 0;
else if (level.health_bar_entities[i]->timestamp)
{
if (level.health_bar_entities[i]->timestamp < level.time)
{
level.health_bar_entities[i] = nullptr;
*health_byte = 0;
continue;
}
*health_byte = 0b10000000;
}
else
{
// enemy dead
if (!level.health_bar_entities[i]->enemy->inuse || level.health_bar_entities[i]->enemy->health <= 0)
{
// hack for Makron
if (level.health_bar_entities[i]->enemy->monsterinfo.aiflags & AI_DOUBLE_TROUBLE)
{
*health_byte = 0b10000000;
continue;
}
if (level.health_bar_entities[i]->delay)
{
level.health_bar_entities[i]->timestamp = level.time + gtime_t::from_sec(level.health_bar_entities[i]->delay);
*health_byte = 0b10000000;
}
else
{
level.health_bar_entities[i] = nullptr;
*health_byte = 0;
}
continue;
}
else if (level.health_bar_entities[i]->spawnflags.has(SPAWNFLAG_HEALTHBAR_PVS_ONLY) && !gi.inPVS(ent->s.origin, level.health_bar_entities[i]->enemy->s.origin, true))
{
*health_byte = 0;
continue;
}
float health_remaining = ((float) level.health_bar_entities[i]->enemy->health) / level.health_bar_entities[i]->enemy->max_health;
*health_byte = ((byte) (health_remaining * 0b01111111)) | 0b10000000;
}
}
// ZOID
SetCTFStats(ent);
// ZOID
}
/*
===============
G_CheckChaseStats
===============
*/
void G_CheckChaseStats(edict_t *ent)
{
gclient_t *cl;
for (uint32_t i = 1; i <= game.maxclients; i++)
{
cl = g_edicts[i].client;
if (!g_edicts[i].inuse || cl->chase_target != ent)
continue;
cl->ps.stats = ent->client->ps.stats;
G_SetSpectatorStats(g_edicts + i);
}
}
/*
===============
G_SetSpectatorStats
===============
*/
void G_SetSpectatorStats(edict_t *ent)
{
gclient_t *cl = ent->client;
if (!cl->chase_target)
G_SetStats(ent);
cl->ps.stats[STAT_SPECTATOR] = 1;
// layouts are independant in spectator
cl->ps.stats[STAT_LAYOUTS] = 0;
if (cl->pers.health <= 0 || level.intermissiontime || cl->showscores)
cl->ps.stats[STAT_LAYOUTS] |= LAYOUTS_LAYOUT;
if (cl->showinventory && cl->pers.health > 0)
cl->ps.stats[STAT_LAYOUTS] |= LAYOUTS_INVENTORY;
if (cl->chase_target && cl->chase_target->inuse)
cl->ps.stats[STAT_CHASE] = CS_PLAYERSKINS +
(cl->chase_target - g_edicts) - 1;
else
cl->ps.stats[STAT_CHASE] = 0;
}