diff --git a/README.md b/README.md index 88dc980..9be3cc7 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Muff Mode includes the game logic, a server config, bot files and some map entit - Current weapon is now droppable - Smart weapon auto-switch: now switches to SSG from SG, CG from MG, never auto-switches to chainfist. - Instant gametype changing (eg: from FFA to TDM) + - DuelFire Damage has been changed to Haste: 50% faster movement, 50% faster weapon rate of fire. - Many more! ## Rulesets @@ -140,14 +141,20 @@ Use **[command] [arg]** for the below listed admin commands: - **readyall**: force all players to ready status (during readying warmup status) - **unreadyall**: force all players to NOT ready status (during readying warmup status) -### Client Commands +### Client Commands - Player Configuration Use **[command] [arg]** for the below listed client commands: + - **announcer**: toggles support of QL match announcer events (uses vo_evil set, needs converting to 22KHz PCM WAV) + - **fm**: toggle frag messages - **help**: toggle help text drawing - **id**: toggle crosshair ID drawing - - **fm**: toggle frag messages - **kb**: toggle kill beeps - **timer**: toggle match timer drawing + +### Client Commands - Gameplay - **hook/unhook**: hook/unhook off-hand grapple + - **followkiller** : auto-follow killers when spectating (disabled by default) + - **followleader** : when spectating, auto-follows leading player + - **followpowerup** : auto-follows player picking up powerups when spectating (disabled by default) - **forfeit**: forfeits a match (currently only in duels, requires g_allow_forfeit 1). - **ready/notready**: sets ready status. - **readyup**: toggles ready status. @@ -162,9 +169,9 @@ Use **[command] [arg]** for the below listed client commands: - **auto/a**: auto-select team - **free/f**: join free team (non-team games) - **spectator/s**: spectate + - **time-in** : cuts a time out short + - **time-out** : call a time out, only 1 allowed per player and lasts for value set by g_dm_timeout_length (in seconds). **g_dm_timeout_length 0** disables time outs - **follow [clientname/clientnum]**: follow a specific player. - - **followkiller** : auto-follow killers when spectating (disabled by default) - - **followpowerup** : auto-follows player picking up powerups when spectating (disabled by default) ### Vote Commands Use **callvote [command] [arg]** for the below listed vote commands: @@ -208,6 +215,7 @@ Use **callvote [command] [arg]** for the below listed vote commands: - **g_dm_do_readyup**: Enforce players to ready up to progress from match warmup stage (requires g_dm_do_warmup 1). (default 0) - **g_dm_do_warmup**: Allow match warmup stage. (default 1) - **g_dm_force_join**: replaces g_teamplay_force_join, the menu forces the cvar change so this gets around that, it now applies to regular DM too so the change makes sense. + - **g_dm_holdable_adrenaline** : when set to 1, allows holdable Adrenaline during deathmatch (default 1) - **g_dm_no_self_damage**: when set to 1, disables any self damage after calculating knockback (default: 0) - **g_dm_overtime**: Set stoppage time for each overtime session in seconds. Currently only applies to Duels. (default 120) - **g_dm_powerup_drop**: when set to 1, drops carried powerups upon death (default: 1) @@ -242,7 +250,7 @@ Use **callvote [command] [arg]** for the below listed vote commands: - **g_match_lock**: when set to 1, prohibits joining the match while in progress (default 0) - **g_motd_filename**: points to filename of message of the day file, reverts to default when blank (default motd.txt) - **g_mover_speed_scale**: sets speed scaling factor for all movers in maps (doors, rotators, lifts etc.) (default: 1.0f) - - **g_no_powerups**: disable powerup pickups (Quad, Protection, Double, DuelFire, Invisibility, etc.) + - **g_no_powerups**: disable powerup pickups (Quad, Protection, Double, Haste, Invisibility, etc.) - **g_owner_auto_join**: when set to 0, avoids auto-joining a match as lobby owner (default 1) - **g_round_countdown**: sets round countdown time (in seconds) in round-based gametypes (default 10) - **g_ruleset**: gameplay rules (default 2): @@ -435,9 +443,6 @@ Some entity overrides are included which add some subtle ambient sounds, mover s - Gladiator bots - Menu overhaul, adding voting, full admin controls, mymap, player config -## FIXME: -- the occasional bugged lifts persist with no fix in sight :( - ## Credits: - The Stingy Hat Games YouTube channel for their excellent modding tutorial, without it I would never be able to compile the damned source! - Nightdive team for the impressive remaster, also some on the team who patiently answered all my annoying modding questions (particularly Paril, sponge, Edward850) diff --git a/src/bg_local.h b/src/bg_local.h index bfc8361..bf568eb 100644 --- a/src/bg_local.h +++ b/src/bg_local.h @@ -96,7 +96,7 @@ enum powerup_t : uint8_t { POWERUP_AM_BOMB, POWERUP_QUAD, - POWERUP_DUELFIRE, + POWERUP_HASTE, POWERUP_PROTECTION, POWERUP_INVISIBILITY, POWERUP_SILENCER, @@ -258,6 +258,8 @@ enum player_stat_t { STAT_MONSTER_COUNT, STAT_ROUND_NUMBER, + STAT_MEDAL, + // don't use; just for verification STAT_LAST }; diff --git a/src/bots/bot_utils.cpp b/src/bots/bot_utils.cpp index 297cbcd..8c7048a 100644 --- a/src/bots/bot_utils.cpp +++ b/src/bots/bot_utils.cpp @@ -34,7 +34,7 @@ static void Player_UpdateState(gentity_t *player) { if (player->client->pu_time_quad > level.time) { player->sv.ent_flags |= SVFL_HAS_DMG_BOOST; - } else if (player->client->pu_time_duelfire > level.time) { + } else if (player->client->pu_time_haste > level.time) { player->sv.ent_flags |= SVFL_HAS_DMG_BOOST; } else if (player->client->pu_time_double > level.time) { player->sv.ent_flags |= SVFL_HAS_DMG_BOOST; diff --git a/src/cg_screen.cpp b/src/cg_screen.cpp index 6a3c7de..83c3e78 100644 --- a/src/cg_screen.cpp +++ b/src/cg_screen.cpp @@ -693,6 +693,28 @@ static void CG_DrawTable(int x, int y, uint32_t width, uint32_t height, int32_t } } +/* +================= +CG_TimeStringMs +================= +*/ +static const char *CG_TimeStringMs(const int msec) { + int hours, mins, seconds, ms = msec; + + seconds = ms / 1000; + ms -= seconds * 1000; + mins = seconds / 60; + seconds -= mins * 60; + hours = mins / 60; + mins -= hours * 60; + + if (hours > 0) { + return G_Fmt("{}:{:02}:{:02}.{}", hours, mins, seconds, ms).data(); + } else { + return G_Fmt("{:02}:{:02}.{}", mins, seconds, ms).data(); + } +} + /* ================ CG_ExecuteLayoutString @@ -816,7 +838,7 @@ static void CG_ExecuteLayoutString(const char *s, vrect_t hud_vrect, vrect_t hud cgi.Com_Error("client >= MAX_CLIENTS"); } - int score, ping; + int score, ping, time; token = COM_Parse(&s); if (!skip_depth) @@ -826,6 +848,11 @@ static void CG_ExecuteLayoutString(const char *s, vrect_t hud_vrect, vrect_t hud if (!skip_depth) { ping = atoi(token); + token = COM_Parse(&s); + time = atoi(token); + + const char *scr = time > 0 ? CG_TimeStringMs(time) : G_Fmt("{}", score).data(); + cgi.SCR_SetAltTypeface(ui_acc_alttypeface->integer && true); if (!scr_usekfont->integer) CG_DrawString(x + 32 * scale, y, scale, cgi.CL_GetClientName(value)); @@ -833,16 +860,16 @@ static void CG_ExecuteLayoutString(const char *s, vrect_t hud_vrect, vrect_t hud cgi.SCR_DrawFontString(cgi.CL_GetClientName(value), x + 32 * scale, y - (font_y_offset * scale), scale, rgba_white, true, text_align_t::LEFT); if (!scr_usekfont->integer) - CG_DrawString(x + 32 * scale, y + 10 * scale, scale, G_Fmt("{}", score).data(), true); + CG_DrawString(x + 32 * scale, y + 10 * scale, scale, scr, true); else - cgi.SCR_DrawFontString(G_Fmt("{}", score).data(), x + 32 * scale, y + (10 - font_y_offset) * scale, scale, rgba_white, true, text_align_t::LEFT); - - cgi.SCR_DrawPic(x + 96 * scale, y + 10 * scale, 9 * scale, 9 * scale, "ping"); + cgi.SCR_DrawFontString(scr, x + 32 * scale, y + (10 - font_y_offset) * scale, scale, rgba_white, true, text_align_t::LEFT); + cgi.SCR_DrawPic(x + 32 + 96 * scale, y + 10 * scale, 9 * scale, 9 * scale, "ping"); if (!scr_usekfont->integer) - CG_DrawString(x + 73 * scale + 32 * scale, y + 10 * scale, scale, G_Fmt("{}", ping).data()); + CG_DrawString(x + 32 + 73 * scale + 32 * scale, y + 10 * scale, scale, G_Fmt("{}", ping).data()); else - cgi.SCR_DrawFontString(G_Fmt("{}", ping).data(), x + 107 * scale, y + (10 - font_y_offset) * scale, scale, rgba_white, true, text_align_t::LEFT); + cgi.SCR_DrawFontString(G_Fmt("{}", ping).data(), x + 32 + 107 * scale, y + (10 - font_y_offset) * scale, scale, rgba_white, true, text_align_t::LEFT); + cgi.SCR_SetAltTypeface(false); } continue; diff --git a/src/g_chase.cpp b/src/g_chase.cpp index 765ee7e..1390814 100644 --- a/src/g_chase.cpp +++ b/src/g_chase.cpp @@ -59,7 +59,7 @@ void UpdateChaseCam(gentity_t *ent) { // mark the chased player as instanced so we can disable their model's visibility targ->svflags |= SVF_INSTANCED; - // copy everything from ps but pmove, pov, stats, and team_id + // copy everything from ps but pmove, pov, stats, and team ent->client->ps.viewangles = targ->client->ps.viewangles; ent->client->ps.viewoffset = targ->client->ps.viewoffset; ent->client->ps.kick_angles = targ->client->ps.kick_angles; @@ -136,6 +136,12 @@ void UpdateChaseCam(gentity_t *ent) { goal = trace.endpos; goal[2] += 6; } + + ent->client->ps.gunindex = 0; + ent->client->ps.gunskin = 0; + ent->s.modelindex = 0; + ent->s.modelindex2 = 0; + ent->s.modelindex3 = 0; } if (targ->deadflag) diff --git a/src/g_cmds.cpp b/src/g_cmds.cpp index c6fbe1d..279b081 100644 --- a/src/g_cmds.cpp +++ b/src/g_cmds.cpp @@ -528,7 +528,7 @@ static void Cmd_TimeIn_f(gentity_t *ent) { return; } - gi.Broadcast_Print(PRINT_HIGH, "Admin is resuming the match.\n"); + gi.LocBroadcast_Print(PRINT_HIGH, "{} is resuming the match.\n", ent->client->pers.netname); level.timeout_in_place = 3_sec; } @@ -640,7 +640,9 @@ static void Cmd_Use_f(gentity_t *ent) { it = GetItemByIndex((item_id_t)atoi(s)); } else { if (!strcmp(s, "holdable")) { - if (ent->client->pers.inventory[IT_DOPPELGANGER]) + if (ent->client->pers.inventory[IT_AMMO_NUKE]) + it = GetItemByIndex(IT_AMMO_NUKE); + else if (ent->client->pers.inventory[IT_DOPPELGANGER]) it = GetItemByIndex(IT_DOPPELGANGER); else if (ent->client->pers.inventory[IT_TELEPORTER]) it = GetItemByIndex(IT_TELEPORTER); @@ -665,6 +667,9 @@ static void Cmd_Use_f(gentity_t *ent) { } index = it->id; + if (IsCombatDisabled() && !(it->flags & IF_WEAPON)) + return; + // Paril: Use_Weapon handles weapon availability if (!(it->flags & IF_WEAPON) && !ent->client->pers.inventory[index]) { gi.LocClient_Print(ent, PRINT_HIGH, "$g_out_of_item", it->pickup_name); @@ -678,78 +683,7 @@ static void Cmd_Use_f(gentity_t *ent) { ValidateSelectedItem(ent); } -#if 0 -void DropPOI(gentity_t *ent) { - vec3_t start, dir; - P_ProjectSource(ent, ent->client->v_angle, { 0, 0, 0 }, start, dir); - - // see who we're aiming at - gentity_t *aiming_at = nullptr; - float best_dist = -9999; - - for (auto player : active_clients()) { - if (player == ent) - continue; - - vec3_t cdir = player->s.origin - start; - float dist = cdir.normalize(); - - float dot = ent->client->v_forward.dot(cdir); - - if (dot < 0.97) - continue; - else if (dist < best_dist) - continue; - - best_dist = dist; - aiming_at = player; - } - - - bool has_a_target = false; - - if (i == GESTURE_POINT) { - for (auto player : active_clients()) { - if (player == ent) - continue; - else if (!OnSameTeam(ent, player)) - continue; - - has_a_target = true; - break; - } - } - if (i == GESTURE_POINT && has_a_target) { - // don't do this stuff if we're flooding - if (CheckFlood(ent)) - return; - - trace_t tr = gi.traceline(start, start + (ent->client->v_forward * 2048), ent, MASK_SHOT & ~CONTENTS_WINDOW); - - uint32_t key = GetUnicastKey(); - - if (tr.fraction != 1.0f) { - // send to all teammates - for (auto player : active_clients()) { - if (player != ent && !OnSameTeam(ent, player)) - continue; - - gi.WriteByte(svc_poi); - gi.WriteShort(POI_PING + (ent->s.number - 1)); - gi.WriteShort(5000); - gi.WritePosition(tr.endpos); - gi.WriteShort(level.pic_ping); - gi.WriteByte(208); - gi.WriteByte(POI_FLAG_NONE); - gi.unicast(player, false); - - gi.local_sound(player, CHAN_AUTO, gi.soundindex("misc/help_marker.wav"), 1.0f, ATTN_NONE, 0.0f, key); - } - } - } -} -#endif /* ================== Cmd_Drop_f @@ -843,17 +777,21 @@ static void Cmd_Drop_f(gentity_t *ent) { uint32_t key = GetUnicastKey(); for (auto ec : active_clients()) { - if (!OnSameTeam(ent, ec)) - continue; - if (ent == ec) continue; - + if (ClientIsPlaying(ec->client) && !OnSameTeam(ent, ec)) + continue; + if (!ClientIsPlaying(ec->client) && !ec->client->follow_target) + continue; + if (!ClientIsPlaying(ec->client) && ec->client->follow_target && !OnSameTeam(ent, ec->client->follow_target)) + continue; + if (!ClientIsPlaying(ec->client) && ec->client->follow_target && ent == ec->client->follow_target) + continue; + gi.WriteByte(svc_poi); gi.WriteShort(POI_PING + (ent->s.number - 1)); gi.WriteShort(5000); gi.WritePosition(ent->s.origin); - //gi.WriteShort(level.pic_ping); gi.WriteShort(gi.imageindex(it->icon)); gi.WriteByte(215); gi.WriteByte(POI_FLAG_NONE); @@ -1132,6 +1070,12 @@ static void Cmd_Kill_f(gentity_t *ent) { if (IsCombatDisabled()) return; + if (GT(GT_RACE)) { + ClientSpawn(ent); + G_PostRespawn(ent); + return; + } + ent->flags &= ~FL_GODMODE; ent->health = 0; @@ -2136,21 +2080,6 @@ static void StopFollowing(gentity_t *ent, bool release) { ent->client->ps.rdflags = RDF_NONE; } -static int itime() { - struct tm *ltime; - time_t gmtime; - - time(&gmtime); - ltime = localtime(&gmtime); - - const char *s; - s = G_Fmt("{}{:02}{:02}{:02}{:02}{:02}", - 1900 + ltime->tm_year, ltime->tm_mon + 1, ltime->tm_mday, ltime->tm_hour, ltime->tm_min, ltime->tm_sec - ).data(); - - return strtoul(s, nullptr, 10); -} - /* ================= SetTeam @@ -2163,7 +2092,7 @@ bool SetTeam(gentity_t *ent, team_t desired_team, bool inactive, bool force, boo if (!force) { if (!ClientIsPlaying(ent->client) && desired_team != TEAM_SPECTATOR) { bool revoke = false; - if (level.match_state == matchst_t::MATCH_IN_PROGRESS && g_match_lock->integer) { + if (level.match_state >= matchst_t::MATCH_COUNTDOWN && g_match_lock->integer) { gi.LocClient_Print(ent, PRINT_HIGH, "Match is locked whilst in progress, no joining permitted now.\n"); revoke = true; } else if (level.num_playing_human_clients >= maxplayers->integer) { @@ -2225,7 +2154,7 @@ bool SetTeam(gentity_t *ent, team_t desired_team, bool inactive, bool force, boo ent->client->resp.ctf_state = 0; ent->client->sess.inactive = inactive; ent->client->sess.inactivity_time = level.time + 1_min; - ent->client->sess.team_join_time = level.time; + ent->client->sess.team_join_time = desired_team == TEAM_SPECTATOR ? 0_sec : level.time; ent->client->resp.team_delay_time = force || !ent->client->sess.initialised ? level.time : level.time + 5_sec; ent->client->sess.spectator_state = desired_team == TEAM_SPECTATOR ? SPECTATOR_FREE : SPECTATOR_NOT; ent->client->sess.spectator_client = 0; @@ -2325,6 +2254,16 @@ static void Cmd_FragMessages_f(gentity_t *ent) { gi.LocClient_Print(ent, PRINT_HIGH, "{} frag messages.\n", ent->client->sess.pc.show_fragmessages ? "Activating" : "Disabling"); } +/* +================= +Cmd_Announcer_f +================= +*/ +static void Cmd_Announcer_f(gentity_t *ent) { + ent->client->sess.pc.use_expanded ^= true; + gi.LocClient_Print(ent, PRINT_HIGH, "Match announcer: {}\n", ent->client->sess.pc.use_expanded ? "ON" : "OFF"); +} + /* ================= Cmd_KillBeep_f @@ -2786,12 +2725,13 @@ void VoteCommandStore(gentity_t *ent) { ec->client->pers.voted = ec == ent ? 1 : 0; ent->client->pers.vote_count++; + AnnouncerSound(world, "vote_now", "misc/pc_up.wav", true); for (auto ec : active_players()) { if (ec->svflags & SVF_BOT) continue; - gi.local_sound(ec, CHAN_AUTO, gi.soundindex("misc/pc_up.wav"), 1, ATTN_NONE, 0); + //gi.local_sound(ec, CHAN_AUTO, gi.soundindex("misc/pc_up.wav"), 1, ATTN_NONE, 0); if (ec->client == level.vote_client) continue; @@ -2980,6 +2920,23 @@ static void Cmd_FollowKiller_f(gentity_t *ent) { gi.LocClient_Print(ent, PRINT_HIGH, "Auto-follow killer: {}\n", ent->client->sess.pc.follow_killer ? "ON" : "OFF"); } +/* +================= +Cmd_FollowLeader_f +================= +*/ +static void Cmd_FollowLeader_f(gentity_t *ent) { + gentity_t *leader = &g_entities[level.sorted_clients[0] + 1]; + ent->client->sess.pc.follow_leader ^= true; + gi.LocClient_Print(ent, PRINT_HIGH, "Auto-follow leader: {}\n", ent->client->sess.pc.follow_leader ? "ON" : "OFF"); + + if (!ClientIsPlaying(ent->client) && ent->client->sess.pc.follow_leader && ent->client->follow_target != leader) { + ent->client->follow_target = leader; + ent->client->follow_update = true; + UpdateChaseCam(ent); + } +} + /* ================= Cmd_FollowPowerup_f @@ -3538,6 +3495,7 @@ static void Cmd_Motd_f(gentity_t *ent) { cmds_t client_cmds[] = { {"admin", Cmd_Admin_f, CF_ALLOW_INT | CF_ALLOW_SPEC}, {"alertall", Cmd_AlertAll_f, CF_ALLOW_SPEC | CF_CHEAT_PROTECT}, + {"announcer", Cmd_Announcer_f, CF_ALLOW_SPEC | CF_ALLOW_DEAD}, {"balance", Cmd_BalanceTeams_f, CF_ADMIN_ONLY | CF_ALLOW_INT | CF_ALLOW_SPEC}, {"boot", Cmd_Boot_f, CF_ADMIN_ONLY | CF_ALLOW_INT | CF_ALLOW_SPEC}, {"callvote", Cmd_CallVote_f, CF_ALLOW_DEAD | CF_ALLOW_SPEC}, @@ -3550,6 +3508,7 @@ cmds_t client_cmds[] = { {"fm", Cmd_FragMessages_f, CF_ALLOW_SPEC | CF_ALLOW_DEAD}, {"follow", Cmd_Follow_f, CF_ALLOW_SPEC | CF_ALLOW_DEAD}, {"followkiller", Cmd_FollowKiller_f, CF_ALLOW_SPEC | CF_ALLOW_DEAD}, + {"followleader", Cmd_FollowLeader_f, CF_ALLOW_SPEC | CF_ALLOW_DEAD}, {"followpowerup", Cmd_FollowPowerup_f, CF_ALLOW_SPEC | CF_ALLOW_DEAD}, {"forcevote", Cmd_ForceVote_f, CF_ADMIN_ONLY | CF_ALLOW_INT | CF_ALLOW_SPEC}, {"forfeit", Cmd_Forfeit_f, CF_ALLOW_DEAD}, @@ -3608,7 +3567,7 @@ cmds_t client_cmds[] = { {"team", Cmd_Team_f, CF_ALLOW_DEAD | CF_ALLOW_SPEC}, {"teleport", Cmd_Teleport_f, CF_ALLOW_SPEC | CF_CHEAT_PROTECT}, {"time-out", Cmd_TimeOut_f, CF_ALLOW_DEAD | CF_ALLOW_SPEC}, - {"time-in", Cmd_TimeIn_f, CF_ADMIN_ONLY | CF_ALLOW_DEAD | CF_ALLOW_SPEC}, + {"time-in", Cmd_TimeIn_f, CF_ALLOW_DEAD | CF_ALLOW_SPEC}, {"timer", Cmd_Timer_f, CF_ALLOW_SPEC | CF_ALLOW_DEAD}, {"unhook", Cmd_UnHook_f, CF_NONE}, {"unlockteam", Cmd_UnlockTeam_f, CF_ADMIN_ONLY | CF_ALLOW_INT | CF_ALLOW_SPEC}, diff --git a/src/g_items.cpp b/src/g_items.cpp index 4c81d5d..d268c4a 100644 --- a/src/g_items.cpp +++ b/src/g_items.cpp @@ -31,8 +31,8 @@ void Weapon_ProxLauncher(gentity_t *ent); void Use_Quad(gentity_t *ent, gitem_t *item); static gtime_t quad_drop_timeout_hack; -void Use_DuelFire(gentity_t *ent, gitem_t *item); -static gtime_t duelfire_drop_timeout_hack; +void Use_Haste(gentity_t *ent, gitem_t *item); +static gtime_t haste_drop_timeout_hack; void Use_Double(gentity_t *ent, gitem_t *item); static gtime_t double_drop_timeout_hack; void Use_Invisibility(gentity_t *ent, gitem_t *item); @@ -1467,10 +1467,11 @@ THINK(RespawnItem) (gentity_t *ent) -> void { if ((RS(RS_MM) || RS(RS_Q3A)) && deathmatch->integer) { if (ent->item->flags & IF_POWERUP) { - if (RS(RS_MM)) - gi.LocBroadcast_Print(PRINT_HIGH, "{} has spawned!\n", ent->item->pickup_name); + //if (RS(RS_MM)) + // gi.LocBroadcast_Print(PRINT_HIGH, "{} has spawned!\n", ent->item->pickup_name); - gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("misc/alarm.wav"), 1, ATTN_NONE, 0); + //gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("misc/alarm.wav"), 1, ATTN_NONE, 0); //poweruprespawn + QLSound(ent, "items/poweruprespawn", "misc/alarm.wav", true); } } } @@ -1592,6 +1593,8 @@ static bool Pickup_Powerup(gentity_t *ent, gentity_t *other) { other->client->pers.inventory[ent->item->id]++; if (g_quadhog->integer && ent->item->id == IT_POWERUP_QUAD) { + if (ent->item->use) + ent->item->use(other, ent->item); G_FreeEntity(ent); return true; } @@ -1606,8 +1609,8 @@ static bool Pickup_Powerup(gentity_t *ent, gentity_t *other) { quad_drop_timeout_hack = t; use = true; break; - case IT_POWERUP_DUELFIRE: - duelfire_drop_timeout_hack = t; + case IT_POWERUP_HASTE: + haste_drop_timeout_hack = t; use = true; break; case IT_POWERUP_PROTECTION: @@ -1882,8 +1885,8 @@ static void Drop_General(gentity_t *ent, gitem_t *item) { case IT_POWERUP_QUAD: ent->client->pu_time_quad = 0_ms; break; - case IT_POWERUP_DUELFIRE: - ent->client->pu_time_duelfire = 0_ms; + case IT_POWERUP_HASTE: + ent->client->pu_time_haste = 0_ms; break; case IT_POWERUP_PROTECTION: ent->client->pu_time_protection = 0_ms; @@ -2058,17 +2061,17 @@ static bool Pickup_Pack(gentity_t *ent, gentity_t *other) { //====================================================================== -static void Use_Powerup_BroadcastMsg(gentity_t *ent, gitem_t *item, const char *sound_name) { +static void Use_Powerup_BroadcastMsg(gentity_t *ent, gitem_t *item, const char *sound_name, const char *announcer_name) { if (deathmatch->integer) { - //if (RS(RS_MM)) { - if (g_quadhog->integer && item->id == IT_POWERUP_QUAD) { - gi.LocBroadcast_Print(PRINT_CENTER, "{} is the Quad Hog!\n", ent->client->resp.netname); - //} else { - // gi.LocBroadcast_Print(PRINT_HIGH, "{} got the {}!\n", ent->client->resp.netname, item->pickup_name); - } - //} - if (RS(RS_MM) || RS(RS_Q3A)) + if (g_quadhog->integer && item->id == IT_POWERUP_QUAD) { + gi.LocBroadcast_Print(PRINT_CENTER, "{} is the Quad Hog!\n", ent->client->resp.netname); + //} else { + // gi.LocBroadcast_Print(PRINT_HIGH, "{} got the {}!\n", ent->client->resp.netname, item->pickup_name); + } + if (RS(RS_MM) || RS(RS_Q3A)) { gi.sound(ent, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(sound_name), 1, ATTN_NONE, 0); + AnnouncerSound(world, announcer_name, nullptr, false); + } } } @@ -2086,25 +2089,25 @@ void Use_Quad(gentity_t *ent, gitem_t *item) { ent->client->pu_time_quad = max(level.time, ent->client->pu_time_quad) + timeout; - Use_Powerup_BroadcastMsg(ent, item, "items/damage.wav"); + Use_Powerup_BroadcastMsg(ent, item, "items/damage.wav", "quad_damage"); } // ===================================================================== -void Use_DuelFire(gentity_t *ent, gitem_t *item) { +void Use_Haste(gentity_t *ent, gitem_t *item) { gtime_t timeout; ent->client->pers.inventory[item->id]--; - if (duelfire_drop_timeout_hack) { - timeout = duelfire_drop_timeout_hack; - duelfire_drop_timeout_hack = 0_ms; + if (haste_drop_timeout_hack) { + timeout = haste_drop_timeout_hack; + haste_drop_timeout_hack = 0_ms; } else { timeout = 30_sec; } - ent->client->pu_time_duelfire = max(level.time, ent->client->pu_time_duelfire) + timeout; + ent->client->pu_time_haste = max(level.time, ent->client->pu_time_haste) + timeout; - Use_Powerup_BroadcastMsg(ent, item, "items/quadfire1.wav"); + Use_Powerup_BroadcastMsg(ent, item, "items/quadfire1.wav", "haste"); } //====================================================================== @@ -2123,7 +2126,7 @@ static void Use_Double(gentity_t *ent, gitem_t *item) { ent->client->pu_time_double = max(level.time, ent->client->pu_time_double) + timeout; - Use_Powerup_BroadcastMsg(ent, item, "misc/ddamage1.wav"); + Use_Powerup_BroadcastMsg(ent, item, "misc/ddamage1.wav", nullptr); } //====================================================================== @@ -2156,7 +2159,7 @@ static void Use_Protection(gentity_t *ent, gitem_t *item) { ent->client->pu_time_protection = max(level.time, ent->client->pu_time_protection) + timeout; - Use_Powerup_BroadcastMsg(ent, item, "items/protect.wav"); + Use_Powerup_BroadcastMsg(ent, item, "items/protect.wav", "battlesuit"); } //====================================================================== @@ -2219,7 +2222,7 @@ static void Use_Regeneration(gentity_t *ent, gitem_t *item) { ent->client->pu_time_regeneration = max(level.time, ent->client->pu_time_regeneration) + timeout; - Use_Powerup_BroadcastMsg(ent, item, "items/protect.wav"); + Use_Powerup_BroadcastMsg(ent, item, "items/protect.wav", "regeneration"); } static void Use_Invisibility(gentity_t *ent, gitem_t *item) { @@ -2236,7 +2239,7 @@ static void Use_Invisibility(gentity_t *ent, gitem_t *item) { ent->client->pu_time_invisibility = max(level.time, ent->client->pu_time_invisibility) + timeout; - Use_Powerup_BroadcastMsg(ent, item, "items/protect.wav"); + Use_Powerup_BroadcastMsg(ent, item, "items/protect.wav", "invisibility"); } //====================================================================== @@ -2759,7 +2762,7 @@ TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool oth case IT_POWERUP_QUAD: case IT_POWERUP_DOUBLE: case IT_POWERUP_PROTECTION: - case IT_POWERUP_DUELFIRE: + case IT_POWERUP_HASTE: case IT_POWERUP_INVISIBILITY: case IT_POWERUP_REGEN: case IT_FLAG_RED: @@ -2769,21 +2772,24 @@ TOUCH(Touch_Item) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool oth for (auto ec : active_clients()) { if (other == ec) continue; + if (ent == ec) + continue; + if (ClientIsPlaying(ec->client) && !OnSameTeam(ent, ec)) + continue; + if (!ClientIsPlaying(ec->client) && ec->client->follow_target && !OnSameTeam(ent, ec->client->follow_target)) + continue; - if (!ClientIsPlaying(ec->client) || (Teams() && ec->client->sess.team == other->client->sess.team)) { - gi.WriteByte(svc_poi); - gi.WriteShort(POI_PING + (ent->s.number - 1)); - gi.WriteShort(5000); - gi.WritePosition(other->s.origin); - //gi.WriteShort(level.pic_ping); - gi.WriteShort(gi.imageindex(it->icon)); - gi.WriteByte(215); - gi.WriteByte(POI_FLAG_NONE); - gi.unicast(ec, false); - gi.local_sound(ec, CHAN_AUTO, gi.soundindex("misc/help_marker.wav"), 1.0f, ATTN_NONE, 0.0f, key); - - gi.LocClient_Print(ec, PRINT_TTS, G_Fmt("{}{} got the {}.\n", ec->client->sess.team != TEAM_SPECTATOR ? "[TEAM]: " : "", other->client->resp.netname, it->use_name).data()); - } + gi.WriteByte(svc_poi); + gi.WriteShort(POI_PING + (ent->s.number - 1)); + gi.WriteShort(5000); + gi.WritePosition(other->s.origin); + gi.WriteShort(gi.imageindex(it->icon)); + gi.WriteByte(215); + gi.WriteByte(POI_FLAG_NONE); + gi.unicast(ec, false); + gi.local_sound(ec, CHAN_AUTO, gi.soundindex("misc/help_marker.wav"), 1.0f, ATTN_NONE, 0.0f, key); + + gi.LocClient_Print(ec, PRINT_TTS, G_Fmt("{}{} got the {}.\n", ec->client->sess.team != TEAM_SPECTATOR ? "[TEAM]: " : "", other->client->resp.netname, it->use_name).data()); } //BroadcastFriendlyMessage(other->client->sess.team, G_Fmt("{} got the {}.\n", other->client->resp.netname, it->use_name).data()); @@ -4596,10 +4602,10 @@ model="models/items/quaddama/tris.md2" model="models/items/quadfire/tris.md2" */ { - /* id */ IT_POWERUP_DUELFIRE, + /* id */ IT_POWERUP_HASTE, /* classname */ "item_quadfire", /* pickup */ Pickup_Powerup, - /* use */ Use_DuelFire, + /* use */ Use_Haste, /* drop */ Drop_General, /* weaponthink */ nullptr, /* pickup_sound */ "items/pkup.wav", @@ -4607,16 +4613,16 @@ model="models/items/quadfire/tris.md2" /* world_model_flags */ EF_ROTATE | EF_BOB, /* view_model */ nullptr, /* icon */ "p_quadfire", - /* use_name */ "DualFire Damage", - /* pickup_name */ "$item_dualfire_damage", - /* pickup_name_definite */ "$item_dualfire_damage_def", + /* use_name */ "Haste", + /* pickup_name */ "Haste", + /* pickup_name_definite */ "Haste", /* quantity */ 60, /* ammo */ IT_NULL, /* chain */ IT_NULL, /* flags */ IF_POWERUP | IF_POWERUP_WHEEL, /* vwep_model */ nullptr, /* armor_info */ nullptr, - /* tag */ POWERUP_DUELFIRE, + /* tag */ POWERUP_HASTE, /* precaches */ "items/quadfire1.wav items/quadfire2.wav items/quadfire3.wav" }, diff --git a/src/g_local.h b/src/g_local.h index 3dacb7d..d00ab30 100644 --- a/src/g_local.h +++ b/src/g_local.h @@ -10,7 +10,7 @@ constexpr const char *GAMEVERSION = "baseq2"; constexpr const char *GAMEMOD_TITLE = "Muff Mode BETA"; -constexpr const char *GAMEMOD_VERSION = "0.19.3"; +constexpr const char *GAMEMOD_VERSION = "0.19.50"; //================================================================== @@ -24,13 +24,135 @@ constexpr size_t MAX_CLIENTS_KEX = 32; // absolute limit enum mstats_t : uint32_t { MSTAT_NONE, - MSTAT_KILLS, - MSTAT_DEATHS, + MSTAT_KILLS_TOTAL, + MSTAT_KILLS_SPAWN, + MSTAT_DEATHS_TOTAL, + MSTAT_DEATHS_SUICIDES, + MSTAT_DEATHS_ENVIRO, + MSTAT_DEATHS_SPAWN, MSTAT_SHOTS, MSTAT_HITS, MSTAT_DMG_DEALT, MSTAT_DMG_RECEIVED, + MSTAT_PING_PEAK, + MSTAT_PING_TRACKER, + MSTAT_PING_TICKS, + + MSTAT_HEALTH_PEAK, + MSTAT_HEALTH_TRACKER, + MSTAT_HEALTH_TICKS, +#if 0 + MSTAT_PKUP_MEGA_COUNT, + MSTAT_PKUP_MEGA_TIMER, + MSTAT_PKUP_YA_COUNT, + MSTAT_PKUP_YA_TIMER, + MSTAT_PKUP_RA_COUNT, + MSTAT_PKUP_RA_TIMER, + MSTAT_PKUP_QUAD_COUNT, + MSTAT_PKUP_QUAD_TIMER, + MSTAT_PKUP_DOUBLER_COUNT, + MSTAT_PKUP_DOUBLER_TIMER, + MSTAT_PKUP_PROTECTION_COUNT, + MSTAT_PKUP_PROTECTION_TIMER, + MSTAT_PKUP_INVIS_COUNT, + MSTAT_PKUP_INVIS_TIMER, + MSTAT_PKUP_DUELFIRE_COUNT, + MSTAT_PKUP_DUELFIRE_TIMER, + MSTAT_PKUP_ADRENALINE_COUNT, + MSTAT_PKUP_ADRENALINE_TIMER, + + MSTAT_WP_BL_SHOTS, + MSTAT_WP_BL_HITS, + MSTAT_WP_BL_KILLS, + MSTAT_WP_BL_DEATHS, + MSTAT_WP_BL_DMGD, + MSTAT_WP_BL_DMGR, + MSTAT_WP_SG_SHOTS, + MSTAT_WP_SG_HITS, + MSTAT_WP_SG_KILLS, + MSTAT_WP_SG_DEATHS, + MSTAT_WP_SG_DMGD, + MSTAT_WP_SG_DMGR, + MSTAT_WP_SSG_SHOTS, + MSTAT_WP_SSG_HITS, + MSTAT_WP_SSG_KILLS, + MSTAT_WP_SSG_DEATHS, + MSTAT_WP_SSG_DMGD, + MSTAT_WP_SSG_DMGR, + MSTAT_WP_MG_SHOTS, + MSTAT_WP_MG_HITS, + MSTAT_WP_MG_KILLS, + MSTAT_WP_MG_DEATHS, + MSTAT_WP_MG_DMGD, + MSTAT_WP_MG_DMGR, + MSTAT_WP_CG_SHOTS, + MSTAT_WP_CG_HITS, + MSTAT_WP_CG_KILLS, + MSTAT_WP_CG_DEATHS, + MSTAT_WP_CG_DMGD, + MSTAT_WP_CG_DMGR, + MSTAT_WP_HG_SHOTS, + MSTAT_WP_HG_HITS, + MSTAT_WP_HG_KILLS, + MSTAT_WP_HG_DEATHS, + MSTAT_WP_HG_DMGD, + MSTAT_WP_HG_DMGR, + MSTAT_WP_GL_SHOTS, + MSTAT_WP_GL_HITS, + MSTAT_WP_GL_KILLS, + MSTAT_WP_GL_DEATHS, + MSTAT_WP_GL_DMGD, + MSTAT_WP_GL_DMGR, + MSTAT_WP_RL_SHOTS, + MSTAT_WP_RL_HITS, + MSTAT_WP_RL_KILLS, + MSTAT_WP_RL_DEATHS, + MSTAT_WP_RL_DMGD, + MSTAT_WP_RL_DMGR, + MSTAT_WP_HB_SHOTS, + MSTAT_WP_HB_HITS, + MSTAT_WP_HB_KILLS, + MSTAT_WP_HB_DEATHS, + MSTAT_WP_HB_DMGD, + MSTAT_WP_HB_DMGR, + MSTAT_WP_RG_SHOTS, + MSTAT_WP_RG_HITS, + MSTAT_WP_RG_KILLS, + MSTAT_WP_RG_DEATHS, + MSTAT_WP_RG_DMGD, + MSTAT_WP_RG_DMGR, + MSTAT_WP_PB_SHOTS, + MSTAT_WP_PB_HITS, + MSTAT_WP_PB_KILLS, + MSTAT_WP_PB_DEATHS, + MSTAT_WP_PB_DMGD, + MSTAT_WP_PB_DMGR, + MSTAT_WP_TM_SHOTS, + MSTAT_WP_TM_HITS, + MSTAT_WP_TM_KILLS, + MSTAT_WP_TM_DEATHS, + MSTAT_WP_TM_DMGD, + MSTAT_WP_TM_DMGR, + MSTAT_WP_PL_SHOTS, + MSTAT_WP_PL_HITS, + MSTAT_WP_PL_KILLS, + MSTAT_WP_PL_DEATHS, + MSTAT_WP_PL_DMGD, + MSTAT_WP_PL_DMGR, + MSTAT_WP_IR_SHOTS, + MSTAT_WP_IR_HITS, + MSTAT_WP_IR_KILLS, + MSTAT_WP_IR_DEATHS, + MSTAT_WP_IR_DMGD, + MSTAT_WP_IR_DMGR, + MSTAT_WP_BFG_SHOTS, + MSTAT_WP_BFG_HITS, + MSTAT_WP_BFG_KILLS, + MSTAT_WP_BFG_DEATHS, + MSTAT_WP_BFG_DMGD, + MSTAT_WP_BFG_DMGR, +#endif MSTAT_TOTAL }; @@ -114,6 +236,7 @@ enum gtf_t { GTF_ARENA = 0x04, GTF_ROUNDS = 0x08, GTF_ELIMINATION = 0x10, + GTF_FRAGS = 0x20 }; extern int _gt[GT_NUM_GAMETYPES]; @@ -208,6 +331,19 @@ enum playerspawn_t { SPAWN_NEAREST }; +enum medal_t : uint8_t { + MEDAL_NONE, + MEDAL_EXCELLENT, + MEDAL_HUMILIATION, + MEDAL_IMPRESSIVE, + MEDAL_RAMPAGE, + MEDAL_DEFENCE, + MEDAL_ASSIST, + MEDAL_CAPTURE, + + MEDAL_TOTAL +}; + #define RANK_TIED_FLAG 0x4000 typedef enum { @@ -1050,7 +1186,7 @@ enum item_id_t : int32_t { IT_AMMO_ROUNDS, IT_POWERUP_QUAD, - IT_POWERUP_DUELFIRE, + IT_POWERUP_HASTE, IT_POWERUP_PROTECTION, IT_POWERUP_INVISIBILITY, IT_POWERUP_SILENCER, @@ -1312,7 +1448,7 @@ struct game_locals_t { gametype_t gametype; std::string motd; - int motd_modcount = 0; + int motd_mod_count = 0; ruleset_t ruleset; @@ -1515,6 +1651,7 @@ struct level_locals_t { int num_playing_blue; int team_scores[TEAM_NUM_TEAMS]; + int team_old_scores[TEAM_NUM_TEAMS]; matchst_t match_state; warmupreq_t warmup_requisite; @@ -1573,6 +1710,12 @@ struct level_locals_t { gtime_t timeout_in_place; gentity_t *timeout_ent; + + std::string match_id; + + bool frag_warning[3]; + + bool prepare_to_fight; }; struct shadow_light_temp_t { @@ -2266,7 +2409,6 @@ extern cvar_t *g_dm_intermission_shots; extern cvar_t *g_dm_item_respawn_rate; extern cvar_t *g_dm_no_fall_damage; extern cvar_t *g_dm_no_quad_drop; -extern cvar_t *g_dm_no_quadfire_drop; extern cvar_t *g_dm_no_self_damage; extern cvar_t *g_dm_no_stack_double; extern cvar_t *g_dm_overtime; @@ -2523,7 +2665,14 @@ bool InCoopStyle(); gentity_t *ClientEntFromString(const char *in); ruleset_t RS_IndexFromString(const char *in); void TeleporterVelocity(gentity_t *ent, gvec3_t angles); +int MS_Value(gclient_t *cl, mstats_t index); void MS_Adjust(gclient_t *cl, mstats_t index, int count); +void MS_AdjustDuo(gclient_t *cl, mstats_t index1, mstats_t index2, int count); +void MS_Set(gclient_t *cl, mstats_t index, int value); +const char *stime(); +void AnnouncerSound(gentity_t *ent, const char *announcer_sound, const char *backup_sound, bool use_backup); +void QLSound(gentity_t *ent, const char *ql_sound, const char *backup_sound, bool use_backup); +void G_StuffCmd(gentity_t *e, const char *fmt, ...); // // g_spawn.cpp @@ -2947,8 +3096,6 @@ constexpr gtime_t GRENADE_TIMER = 3_sec; constexpr float GRENADE_MINSPEED = 400.f; constexpr float GRENADE_MAXSPEED = 800.f; -extern bool is_quad; -extern bool is_quadfire; extern player_muzzle_t is_silenced; extern byte damage_multiplier; @@ -3252,6 +3399,15 @@ struct client_persistant_t { bool holdable_item_msg_adren; bool holdable_item_msg_tele; bool holdable_item_msg_doppel; + + gtime_t medal_time; + medal_t medal_type; + uint32_t medal_count[MEDAL_TOTAL]; + + bool rail_hit; + gtime_t kill_time; + + gtime_t last_spawn_time; }; // player config vars: @@ -3262,7 +3418,10 @@ struct client_config_t { int killbeep_num; bool follow_killer; + bool follow_leader; bool follow_powerup; + + bool use_expanded; }; // client data that stays across deathmatch level changes, handled differently to client_persistent_t @@ -3325,7 +3484,7 @@ struct client_respawn_t { int motd_mod_count; bool showed_help; - int rank; + int rank, old_rank; char netname[MAX_NETNAME]; gtime_t team_delay_time; @@ -3438,7 +3597,7 @@ struct gclient_t { // powerup timers gtime_t pu_time_quad; - gtime_t pu_time_duelfire; + gtime_t pu_time_haste; gtime_t pu_time_double; gtime_t pu_time_protection; gtime_t pu_time_invisibility; @@ -3463,6 +3622,8 @@ struct gclient_t { gtime_t respawn_min_time; // can't respawn before time > this gtime_t respawn_time; // can respawn when time > this + gentity_t *follow_queued_target; + gtime_t follow_queued_time; gentity_t *follow_target; // player we are following bool follow_update; // need to update follow info? diff --git a/src/g_main.cpp b/src/g_main.cpp index ec9af7c..4308ca9 100644 --- a/src/g_main.cpp +++ b/src/g_main.cpp @@ -121,7 +121,6 @@ cvar_t *g_dm_intermission_shots; cvar_t *g_dm_item_respawn_rate; cvar_t *g_dm_no_fall_damage; cvar_t *g_dm_no_quad_drop; -cvar_t *g_dm_no_quadfire_drop; cvar_t *g_dm_no_self_damage; cvar_t *g_dm_no_stack_double; cvar_t *g_dm_overtime; @@ -253,9 +252,9 @@ void InitSave(); int _gt[] = { /* GT_NONE */ 0, - /* GT_FFA */ 0, - /* GT_DUEL */ 0, - /* GT_TDM */ GTF_TEAMS, + /* GT_FFA */ GTF_FRAGS, + /* GT_DUEL */ GTF_FRAGS, + /* GT_TDM */ GTF_TEAMS | GTF_FRAGS, /* GT_CTF */ GTF_TEAMS | GTF_CTF, /* GT_CA */ GTF_TEAMS | GTF_ARENA | GTF_ROUNDS | GTF_ELIMINATION, /* GT_FREEZE */ GTF_TEAMS | GTF_ELIMINATION, @@ -579,7 +578,7 @@ void G_LoadMOTD() { if (valid) { game.motd = (const char *)buffer; - game.motd_modcount++; + game.motd_mod_count++; if (g_verbose->integer) gi.Com_PrintFmt("{}: MotD file verified and loaded: \"{}\"\n", __FUNCTION__, name); } else { @@ -978,7 +977,6 @@ static void InitGame() { g_dm_item_respawn_rate = gi.cvar("g_dm_item_respawn_rate", "1.0", CVAR_NOFLAGS); g_dm_no_fall_damage = gi.cvar("g_dm_no_fall_damage", "0", CVAR_NOFLAGS); g_dm_no_quad_drop = gi.cvar("g_dm_no_quad_drop", "0", CVAR_NOFLAGS); - g_dm_no_quadfire_drop = gi.cvar("g_dm_no_quadfire_drop", "0", CVAR_NOFLAGS); g_dm_no_self_damage = gi.cvar("g_dm_no_self_damage", "0", CVAR_NOFLAGS); g_dm_no_stack_double = gi.cvar("g_dm_no_stack_double", "0", CVAR_NOFLAGS); g_dm_overtime = gi.cvar("g_dm_overtime", "120", CVAR_NOFLAGS); @@ -1231,6 +1229,7 @@ static void Entities_Reset(bool reset_players, bool reset_ghost, bool reset_scor ec->client->eliminated = false; ec->client->respawn_time = level.time; // +random_time(1_sec, 4_sec); + ec->client->pers.last_spawn_time = level.time; ec->client->ps.pmove.pm_type = PM_DEAD; ec->client->anim_priority = ANIM_DEATH; ec->s.frame = FRAME_death308 - 1; @@ -1417,6 +1416,8 @@ static bool Round_StartNew() { gi.LocBroadcast_Print(PRINT_CENTER, "{} {}\nBegins in...", horde ? "Wave" : "Round", round_num); } + AnnouncerSound(world, "round_begins_in", nullptr, false); + return true; } @@ -1442,6 +1443,18 @@ void Round_End() { level.horde_all_spawned = false; } +/* +============= +SetMatchID +============= +*/ +static void SetMatchID() { + //level.match_id = gt_short_name_upper[g_gametype->integer]; + //level.match_id += "-"; + level.match_id = stime(); + level.match_id = stime(); +} + /* ============ Match_Start @@ -1474,7 +1487,9 @@ void Match_Start() { Entities_Reset(true, true, true); UnReadyAll(); - gi.LocBroadcast_Print(PRINT_TTS, "The match has started!\n"); + SetMatchID(); + + gi.LocBroadcast_Print(PRINT_TTS, "Match ID: {}\n", level.match_id.c_str()); if (GT(GT_STRIKE)) level.strike_red_attacks = brandom(); @@ -1482,8 +1497,9 @@ void Match_Start() { if (Round_StartNew()) return; - gi.LocBroadcast_Print(PRINT_CENTER, "FIGHT!"); - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/tele_up.wav"), 1, ATTN_NONE, 0); + gi.LocBroadcast_Print(PRINT_CENTER, GT(GT_RACE) ? "GO!" : "FIGHT!"); + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("misc/tele_up.wav"), 1, ATTN_NONE, 0); + AnnouncerSound(world, GT(GT_RACE) ? "go" : "fight", "misc/tele_up.wav", true); } /* @@ -1751,6 +1767,7 @@ static void CheckDMRoundState(void) { bool horde = GT(GT_HORDE); gi.LocBroadcast_Print(PRINT_CHAT, "{} {} has begun!\n", horde ? "Wave" : "Round", level.round_number); gi.LocBroadcast_Print(PRINT_CENTER, horde ? (brandom() ? "INCOMING!" : "LOCK AND LOAD!") : "FIGHT!"); + AnnouncerSound(world, "fight", nullptr, false); if (horde) { level.horde_num_monsters_to_spawn = clamp(15 + (level.round_number * 5), 20, 80); @@ -1790,9 +1807,11 @@ static void CheckDMRoundState(void) { G_AdjustTeamScore(TEAM_BLUE, points); if (GT(GT_STRIKE)) gi.LocBroadcast_Print(PRINT_CENTER, "Turn has ended.\n{} successfully {}!\n", Teams_TeamName(TEAM_BLUE), points ? "attacked" : "defended"); - else + else { gi.LocBroadcast_Print(PRINT_CENTER, "{} wins the round!\n(eliminated {})\n", Teams_TeamName(TEAM_BLUE), Teams_TeamName(TEAM_RED)); - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + AnnouncerSound(world, "blue_wins_round", "ctf/flagcap.wav", true); + } + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); Round_End(); return; } @@ -1801,9 +1820,11 @@ static void CheckDMRoundState(void) { G_AdjustTeamScore(TEAM_RED, points); if (GT(GT_STRIKE)) { gi.LocBroadcast_Print(PRINT_CENTER, "Turn has ended.\n{} successfully {}!\n", Teams_TeamName(TEAM_RED), points ? "attacked" : "defended"); - } else + } else { gi.LocBroadcast_Print(PRINT_CENTER, "{} wins the round!\n(eliminated {})\n", Teams_TeamName(TEAM_RED), Teams_TeamName(TEAM_BLUE)); - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + AnnouncerSound(world, "red_wins_round", "ctf/flagcap.wav", false); + } + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); Round_End(); return; } @@ -1845,11 +1866,13 @@ static void CheckDMRoundState(void) { if (level.num_living_red > level.num_living_blue) { G_AdjustTeamScore(TEAM_RED, 1); gi.LocBroadcast_Print(PRINT_CENTER, "{} wins the round!\n(players remaining: {} vs {})\n", Teams_TeamName(TEAM_RED), level.num_living_red, level.num_living_blue); - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + AnnouncerSound(world, "red_wins_round", "ctf/flagcap.wav", false); } else if (level.num_living_blue > level.num_living_red) { G_AdjustTeamScore(TEAM_BLUE, 1); gi.LocBroadcast_Print(PRINT_CENTER, "{} wins the round!\n(players remaining: {} vs {})\n", Teams_TeamName(TEAM_BLUE), level.num_living_blue, level.num_living_red); - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + AnnouncerSound(world, "blue_wins_round", "ctf/flagcap.wav", true); } else { int total_health_red = 0, total_health_blue = 0; @@ -1869,11 +1892,13 @@ static void CheckDMRoundState(void) { if (total_health_red > total_health_blue) { G_AdjustTeamScore(TEAM_RED, 1); gi.LocBroadcast_Print(PRINT_CENTER, "{} wins the round!\n(total health: {} vs {})\n", Teams_TeamName(TEAM_RED), total_health_red, total_health_blue); - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + AnnouncerSound(world, "red_wins_round", "ctf/flagcap.wav", false); } else if (total_health_blue > total_health_red) { G_AdjustTeamScore(TEAM_BLUE, 1); gi.LocBroadcast_Print(PRINT_CENTER, "{} wins the round!\n(total health: {} vs {})\n", Teams_TeamName(TEAM_BLUE), total_health_blue, total_health_red); - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + AnnouncerSound(world, "blue_wins_round", "ctf/flagcap.wav", true); } else { gi.LocBroadcast_Print(PRINT_CENTER, "Round draw!"); } @@ -1910,7 +1935,12 @@ static void CheckDMCountdown(void) { if (!level.countdown_check || level.countdown_check.seconds() > t) { if (!(t % 10) || t < 10) { - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(G_Fmt("world/{}{}.wav", t, t >= 20 ? "sec" : "").data()), 1, ATTN_NONE, 0); + AnnouncerSound(world, nullptr, G_Fmt("world/{}{}.wav", t, t >= 20 ? "sec" : "").data(), false); + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(G_Fmt("world/{}{}.wav", t, t >= 20 ? "sec" : "").data()), 1, ATTN_NONE, 0); + if (t <= 3) { + const char *s[3] = { "one", "two", "three" }; + AnnouncerSound(world, G_Fmt("{}", s[t-1]).data(), nullptr, false); + } } level.countdown_check = gtime_t::from_sec(t); } @@ -1935,9 +1965,12 @@ static void CheckDMMatchEndWarning(void) { if (!level.matchendwarn_check || level.matchendwarn_check.seconds() > t) { if (t && (t == 30 || t == 20 || t <= 10)) { - gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(G_Fmt("world/{}{}.wav", t, t >= 20 ? "sec" : "").data()), 1, ATTN_NONE, 0); + AnnouncerSound(world, nullptr, G_Fmt("world/{}{}.wav", t, t >= 20 ? "sec" : "").data(), false); + //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(G_Fmt("world/{}{}.wav", t, t >= 20 ? "sec" : "").data()), 1, ATTN_NONE, 0); if (t >= 10) gi.LocBroadcast_Print(PRINT_HIGH, "{} second warning!\n", t); + } else if (t == 300 || t == 60) { + AnnouncerSound(world, G_Fmt("{}_minute", t == 300 ? 5 : 1).data(), nullptr, false); } level.matchendwarn_check = gtime_t::from_sec(t); } @@ -1961,6 +1994,10 @@ static void CheckDMWarmupState(void) { level.warmup_notice_time = 0_sec; return; } + // pull in any spectating bots + for (auto ec : active_clients()) + if (!ClientIsPlaying(ec->client) && (ec->client->sess.is_a_bot || ec->svflags & SVF_BOT)) + SetTeam(ec, PickTeam(-1), false, false, false); return; } @@ -2006,6 +2043,13 @@ static void CheckDMWarmupState(void) { not_enough = true; } + if (notGT(GT_DUEL)) { + // pull in any spectating bots + for (auto ec : active_clients()) + if (!ClientIsPlaying(ec->client) && (ec->client->sess.is_a_bot || ec->svflags & SVF_BOT)) + SetTeam(ec, PickTeam(-1), false, false, false); + } + if (!g_dm_allow_no_humans->integer && !level.num_playing_human_clients) not_enough = true; @@ -2048,6 +2092,7 @@ static void CheckDMWarmupState(void) { level.match_state = matchst_t::MATCH_WARMUP_DEFAULT; level.warmup_requisite = warmupreq_t::WARMUP_REQ_NONE; level.warmup_notice_time = 0_sec; + level.prepare_to_fight = false; return; } @@ -2062,13 +2107,19 @@ static void CheckDMWarmupState(void) { if (g_warmup_countdown->integer > 0) { level.match_state_timer = level.time + gtime_t::from_sec(g_warmup_countdown->integer); - //gi.positioned_sound(world->s.origin, world, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex("world/10_0.wav"), 1, ATTN_NONE, 0); // announce it - if (GT(GT_DUEL) && &game.clients[level.sorted_clients[0]] && &game.clients[level.sorted_clients[1]]) + if ((GT(GT_DUEL) || (level.num_playing_clients == 2 && g_match_lock->integer)) && + &game.clients[level.sorted_clients[0]] && &game.clients[level.sorted_clients[1]]) gi.LocBroadcast_Print(PRINT_CENTER, "{} vs {}\nBegins in...", game.clients[level.sorted_clients[0]].resp.netname, game.clients[level.sorted_clients[1]].resp.netname); else gi.LocBroadcast_Print(PRINT_CENTER, "{}\nBegins in...", level.gametype_name); + + //gi.LocBroadcast_Print(PRINT_HIGH, "{}Match {} starting...\n", g_match_lock->integer ? "TEAMS LOCKED! " : "", level.match_id.data()); + if (!level.prepare_to_fight) { + AnnouncerSound(world, (Teams() && level.num_playing_clients >= 4) ? "prepare_your_team" : "prepare_to_fight", nullptr, false); + level.prepare_to_fight = true; + } } else { level.match_state_timer = 0_ms; goto start; @@ -2107,17 +2158,24 @@ static void CheckVote(void) { if (!level.vote_client) return; + // give it a minimum duration + if (level.time - level.vote_time < 1_sec) + return; + if (level.time - level.vote_time >= 30_sec) { gi.LocBroadcast_Print(PRINT_HIGH, "Vote timed out.\n"); + AnnouncerSound(world, "vote_failed", nullptr, false); } else { int halfpoint = level.num_voting_clients / 2; if (level.vote_yes > halfpoint) { // execute the command, then remove the vote gi.LocBroadcast_Print(PRINT_HIGH, "Vote passed.\n"); level.vote_execute_time = level.time + 3_sec; + AnnouncerSound(world, "vote_passed", nullptr, false); } else if (level.vote_no >= halfpoint) { // same behavior as a timeout gi.LocBroadcast_Print(PRINT_HIGH, "Vote failed.\n"); + AnnouncerSound(world, "vote_failed", nullptr, false); } else { // still waiting for a majority return; @@ -2378,10 +2436,17 @@ static int SortRanks(const void *a, const void *b) { return -1; // then sort by score - if (ca->resp.score > cb->resp.score) - return -1; - if (ca->resp.score < cb->resp.score) - return 1; + if (GT(GT_RACE)) { + if (ca->resp.score > 0 && (ca->resp.score < cb->resp.score)) + return -1; + if (cb->resp.score > 0 && (ca->resp.score > cb->resp.score)) + return 1; + } else { + if (ca->resp.score > cb->resp.score) + return -1; + if (ca->resp.score < cb->resp.score) + return 1; + } // then sort by time if (ca->sess.team_join_time < cb->sess.team_join_time) @@ -2468,6 +2533,14 @@ void CalculateRanks() { } } + for (size_t i = 0; i < level.num_playing_clients; i++) { + if (game.clients[i].pers.connected) { + game.clients[level.sorted_clients[i]].resp.old_rank = game.clients[level.sorted_clients[i]].resp.rank; + } + } + + int lead_score = game.clients[level.sorted_clients[0]].resp.score; + qsort(level.sorted_clients, level.num_connected_clients, sizeof(level.sorted_clients[0]), SortRanks); // set the rank value for all clients that are connected and not spectators @@ -2489,6 +2562,7 @@ void CalculateRanks() { for (size_t i = 0; i < level.num_playing_clients; i++) { if (game.clients[i].pers.connected) { cl = &game.clients[level.sorted_clients[i]]; + cl->resp.old_score = cl->resp.score; new_score = cl->resp.score; if (i == 0 || new_score != score) { rank = i; @@ -2511,6 +2585,99 @@ void CalculateRanks() { level.warmup_notice_time = level.time; + if (level.match_state == MATCH_IN_PROGRESS) { + if (GTF(GTF_FRAGS)) { + //gi.Com_PrintFmt("new={} old={}\n", game.clients[level.sorted_clients[0]].resp.score, old_first_score); + if (fraglimit->integer > 3) { + int score_diff = fraglimit->integer - game.clients[level.sorted_clients[0]].resp.score; + if (score_diff <= 3 && !level.frag_warning[score_diff - 1]) { + AnnouncerSound(world, G_Fmt("{}_frag{}", score_diff, score_diff > 1 ? "s" : "").data(), nullptr, false); + level.frag_warning[score_diff - 1] = true; + CheckDMExitRules(); + return; + } + } + } + if (!Teams() && game.clients[level.sorted_clients[0]].resp.score > 0) { + // check changes in rank to trigger sounds + int new_rank = 0, old_rank = 0; + bool new_tied = false, old_tied = false; + for (auto ec : active_players()) { + new_rank = ec->client->resp.rank; + old_rank = ec->client->resp.old_rank; + + //if (ec == world + 1) + // gi.Com_PrintFmt("new_rank={} old_rank={}\n", new_rank, old_rank); + + if (new_rank == old_rank) + continue; + + if (new_rank & RANK_TIED_FLAG) { + new_rank &= ~RANK_TIED_FLAG; + new_tied = true; + } else { + new_tied = false; + } + if (old_rank & RANK_TIED_FLAG) { + old_rank &= ~RANK_TIED_FLAG; + old_tied = true; + } else { + old_tied = false; + } + + //if (ec == world + 1) + // gi.Com_PrintFmt("new_rank2={} old_rank2={}\n", new_rank, old_rank); + + if (new_rank == 0 && old_tied != new_tied) { + AnnouncerSound(ec, new_tied ? "lead_tied" : "lead_taken", nullptr, false); + + // find and update all spectators who want to follow leader + for (auto ec2 : active_clients()) { + if (!ClientIsPlaying(ec2->client) && ec2->client->sess.pc.follow_leader && ec2->client->follow_target != ec) { + ec2->client->follow_queued_target = ec; + ec2->client->follow_queued_time = level.time; + } + } + } else if (new_rank != 0 && old_rank == 0) { + AnnouncerSound(ec, "lead_lost", nullptr, false); + } + } + } else if (Teams() && GTF(GTF_FRAGS)) { + int new_rank, old_rank; + + if (level.team_old_scores[TEAM_RED] == level.team_old_scores[TEAM_BLUE]) { + old_rank = 2; + } else if (level.team_old_scores[TEAM_RED] > level.team_old_scores[TEAM_BLUE]) { + old_rank = 0; + } else { + old_rank = 1; + } + if (level.team_scores[TEAM_RED] == level.team_scores[TEAM_BLUE]) { + new_rank = 2; + } else if (level.team_scores[TEAM_RED] > level.team_scores[TEAM_BLUE]) { + new_rank = 0; + } else { + new_rank = 1; + } + + if (old_rank == 2 && new_rank != 2) { + //a team just took the lead + AnnouncerSound(world, new_rank ? "blue_leads" : "red_leads", nullptr, false); + } else if (old_rank != 2 && new_rank == 2) { + //teams just tied + AnnouncerSound(world, "teams_tied", nullptr, false); + } + /* + else if ((GTF(GTF_CTF)) && new_rank != 2) { + //a team has scored + AnnouncerSound(world, new_rank ? "blue_scores" : "red_scores", nullptr, false); + } + */ + level.team_old_scores[TEAM_RED] = level.team_scores[TEAM_RED]; + level.team_old_scores[TEAM_BLUE] = level.team_scores[TEAM_BLUE]; + } + } + // see if it is time to end the level CheckDMExitRules(); } @@ -2838,6 +3005,8 @@ void Match_End() { return; } + //MS_EndMatchExport(); + BeginIntermission(ent); } @@ -2990,15 +3159,17 @@ void CheckDMExitRules() { if (GT(GT_DUEL) && g_dm_overtime->integer > 0) { level.overtime += gtime_t::from_sec(g_dm_overtime->integer); gi.LocBroadcast_Print(PRINT_CENTER, "Overtime!\n{} added", G_TimeString(g_dm_overtime->integer * 1000, false)); + AnnouncerSound(world, "overtime", "world/klaxon2.wav", true); play = true; } else if (!level.suddendeath) { gi.LocBroadcast_Print(PRINT_CENTER, "Sudden Death!"); + AnnouncerSound(world, "sudden_death", "world/klaxon2.wav", true); level.suddendeath = true; play = true; } - if (play) - gi.positioned_sound(world->s.origin, world, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("world/klaxon2.wav"), 1, ATTN_NONE, 0); + //if (play) + //gi.positioned_sound(world->s.origin, world, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("world/klaxon2.wav"), 1, ATTN_NONE, 0); return; } @@ -3056,6 +3227,10 @@ void CheckDMExitRules() { // no score limit in horde if (GT(GT_HORDE)) return; + + // no score limit in race + if (GT(GT_RACE)) + return; int scorelimit = GT_ScoreLimit(); if (!scorelimit) return; @@ -3200,8 +3375,13 @@ void BeginIntermission(gentity_t *targ) { //SetIntermissionPoint(); // move all clients to the intermission point - for (auto ec : active_clients()) + for (auto ec : active_clients()) { MoveClientToIntermission(ec); + if (Teams()) + AnnouncerSound(ec, level.team_scores[TEAM_RED] > level.team_scores[TEAM_BLUE] ? "red_wins" : "blue_wins", nullptr, false); + else + AnnouncerSound(ec, ec->client->resp.rank == 0 ? "you_win" : "you_lose", nullptr, false); + } } /* @@ -3214,6 +3394,8 @@ void ExitLevel() { struct tm *ltime; time_t gmtime; + time(&gmtime); + ltime = localtime(&gmtime); time(&gmtime); ltime = localtime(&gmtime); diff --git a/src/g_menu.cpp b/src/g_menu.cpp index 92102ee..23180e0 100644 --- a/src/g_menu.cpp +++ b/src/g_menu.cpp @@ -1182,14 +1182,14 @@ static void G_Menu_Join_Update(gentity_t *ent) { entries[jmenu_teams_join_red].SelectFunc = G_Menu_Join_Team_Red; entries[jmenu_teams_join_blue].SelectFunc = nullptr; } else { - if (level.locked[TEAM_RED] || level.match_state == matchst_t::MATCH_IN_PROGRESS && g_match_lock->integer) { + if (level.locked[TEAM_RED] || level.match_state >= matchst_t::MATCH_COUNTDOWN && g_match_lock->integer) { Q_strlcpy(entries[jmenu_teams_join_red].text, G_Fmt("{} is LOCKED during play", Teams_TeamName(TEAM_RED)).data(), sizeof(entries[jmenu_teams_join_red].text)); entries[jmenu_teams_join_red].SelectFunc = nullptr; } else { Q_strlcpy(entries[jmenu_teams_join_red].text, G_Fmt("Join {} ({}/{})", Teams_TeamName(TEAM_RED), num_red, floor(pmax / 2)).data(), sizeof(entries[jmenu_teams_join_red].text)); entries[jmenu_teams_join_red].SelectFunc = G_Menu_Join_Team_Red; } - if (level.locked[TEAM_BLUE] || level.match_state == matchst_t::MATCH_IN_PROGRESS && g_match_lock->integer) { + if (level.locked[TEAM_BLUE] || level.match_state >= matchst_t::MATCH_COUNTDOWN && g_match_lock->integer) { Q_strlcpy(entries[jmenu_teams_join_blue].text, G_Fmt("{} is LOCKED during play", Teams_TeamName(TEAM_BLUE)).data(), sizeof(entries[jmenu_teams_join_blue].text)); entries[jmenu_teams_join_blue].SelectFunc = nullptr; } else { @@ -1199,7 +1199,7 @@ static void G_Menu_Join_Update(gentity_t *ent) { } } else { - if (level.locked[TEAM_FREE] || level.match_state == matchst_t::MATCH_IN_PROGRESS && g_match_lock->integer) { + if (level.locked[TEAM_FREE] || level.match_state >= matchst_t::MATCH_COUNTDOWN && g_match_lock->integer) { Q_strlcpy(entries[jmenu_free_join].text, "Match LOCKED during play", sizeof(entries[jmenu_free_join].text)); entries[jmenu_free_join].SelectFunc = nullptr; } else if (GT(GT_DUEL) && level.num_playing_clients == 2) { diff --git a/src/g_misc.cpp b/src/g_misc.cpp index 9a99fec..7436f1c 100644 --- a/src/g_misc.cpp +++ b/src/g_misc.cpp @@ -1057,7 +1057,7 @@ static THINK(barrel_burn) (gentity_t *self) -> void { self->nextthink = level.time + FRAME_TIME_S; } -static DIE(barrel_delay) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { +DIE(barrel_delay) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, const vec3_t &point, const mod_t &mod) -> void { // allow "dead" barrels waiting to explode to still receive knockback if (self->think == barrel_burn || self->think == barrel_explode) return; @@ -1085,7 +1085,7 @@ static THINK(barrel_think) (gentity_t *self) -> void { M_WorldEffects(self); } -static THINK(barrel_start) (gentity_t *self) -> void { +THINK(barrel_start) (gentity_t *self) -> void { M_droptofloor(self); self->think = barrel_think; self->nextthink = level.time + FRAME_TIME_S; diff --git a/src/g_save.cpp b/src/g_save.cpp index e7f3099..268212c 100644 --- a/src/g_save.cpp +++ b/src/g_save.cpp @@ -820,7 +820,7 @@ FIELD_AUTO(pu_time_regeneration), FIELD_AUTO(grenade_blew_up), FIELD_AUTO(grenade_time), FIELD_AUTO(grenade_finished_time), -FIELD_AUTO(pu_time_duelfire), +FIELD_AUTO(pu_time_haste), FIELD_AUTO(silencer_shots), FIELD_AUTO(weapon_sound), diff --git a/src/g_spawn.cpp b/src/g_spawn.cpp index 59f1a0b..05fd0b8 100644 --- a/src/g_spawn.cpp +++ b/src/g_spawn.cpp @@ -526,6 +526,8 @@ void ED_CallSpawn(gentity_t *ent) { ent->classname = GetItemByIndex(IT_AMMO_FLECHETTES)->classname; else if (!strcmp(ent->classname, "weapon_heatbeam")) ent->classname = GetItemByIndex(IT_WEAPON_PLASMABEAM)->classname; + else if (!strcmp(ent->classname, "item_haste")) + ent->classname = GetItemByIndex(IT_POWERUP_HASTE)->classname; else if (RS(RS_Q3A) && !strcmp(ent->classname, "weapon_supershotgun")) ent->classname = GetItemByIndex(IT_WEAPON_SHOTGUN)->classname; else if (!strcmp(ent->classname, "info_player_team1")) @@ -2332,8 +2334,7 @@ void SP_worldspawn(gentity_t *ent) { snd_fry.assign("player/fry.wav"); // standing in lava / slime - if (!deathmatch->integer) - PrecacheItem(GetItemByIndex(IT_COMPASS)); + PrecacheItem(GetItemByIndex(IT_COMPASS)); if (!g_instagib->integer && !g_nadefest->integer && notGT(GT_BALL)) PrecacheItem(GetItemByIndex(IT_WEAPON_BLASTER)); diff --git a/src/g_target.cpp b/src/g_target.cpp index 5676f02..029d436 100644 --- a/src/g_target.cpp +++ b/src/g_target.cpp @@ -366,18 +366,52 @@ static USE(use_target_changelevel) (gentity_t *self, gentity_t *other, gentity_t return; // if noexit, do a ton of damage to other - if (deathmatch->integer && !g_dm_allow_exit->integer && other != world) { + if (deathmatch->integer && g_gametype->integer != GT_RACE && !g_dm_allow_exit->integer && other != world) { T_Damage(other, self, self, vec3_origin, other->s.origin, vec3_origin, 10 * other->max_health, 1000, DAMAGE_NONE, MOD_EXIT); return; } // if multiplayer, let everyone know who hit the exit if (deathmatch->integer) { - if (level.time < 10_sec) + if (g_gametype->integer == GT_RACE) { + if (!IsCombatDisabled()) { + if (level.match_state == MATCH_IN_PROGRESS) { + int old_score = activator->client->resp.score; + int new_score = (level.time - activator->client->respawn_time).milliseconds(); + bool pb = !old_score || new_score < old_score; + if (pb) { + G_SetPlayerScore(activator->client, (level.time - activator->client->respawn_time).milliseconds()); + gi.sound(activator, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex("ctf/flagcap.wav"), 1, ATTN_NONE, 0); + } + gi.LocClient_Print(activator, PRINT_CENTER, "{}{}", G_TimeStringMs(new_score, false), pb ? " (PB)" : ""); + } else { + gi.LocClient_Print(activator, PRINT_CENTER, "{}", G_TimeStringMs((level.time - activator->client->respawn_time).milliseconds(), false)); + } + ClientSpawn(activator); + G_PostRespawn(activator); + + activator->client->pers.last_spawn_time = level.time; + + gtime_t clock = timelimit->value ? (level.match_time + gtime_t::from_min(timelimit->value) + level.overtime - level.time) : level.time - level.match_time; + int t = clock.milliseconds(); + const char *s, *s1, *s2 = ""; + + int t2 = (level.time - activator->client->pers.last_spawn_time).milliseconds(); + s1 = G_Fmt("{} ({})", G_TimeString(t, false), G_TimeStringMs(t2, false)).data(); + + s = G_Fmt("{}{}", s1, s2).data(); + + activator->client->ps.stats[STAT_MATCH_STATE] = CONFIG_MATCH_STATE; + gi.configstring(CONFIG_MATCH_STATE, s); + } return; + } else { + if (level.time < 10_sec) + return; - if (activator && activator->client) - gi.LocBroadcast_Print(PRINT_HIGH, "$g_exited_level", activator->client->pers.netname); + if (activator && activator->client) + gi.LocBroadcast_Print(PRINT_HIGH, "$g_exited_level", activator->client->pers.netname); + } } // if going to a new unit, clear cross triggers @@ -2203,7 +2237,7 @@ static USE(target_remove_powerups_use) (gentity_t *ent, gentity_t *other, gentit return; activator->client->pu_time_quad = 0_sec; - activator->client->pu_time_duelfire = 0_sec; + activator->client->pu_time_haste = 0_sec; activator->client->pu_time_double = 0_sec; activator->client->pu_time_protection = 0_sec; activator->client->pu_time_invisibility = 0_sec; diff --git a/src/g_utils.cpp b/src/g_utils.cpp index 211074d..cbecf3b 100644 --- a/src/g_utils.cpp +++ b/src/g_utils.cpp @@ -979,7 +979,7 @@ void TeleportPlayerToRandomSpawnPoint(gentity_t *ent, bool fx) { } bool InCoopStyle() { - return coop->integer || GT(GT_HORDE); + return coop->integer || GT(GT_HORDE) || GT(GT_RACE); } /* @@ -1032,14 +1032,124 @@ void TeleporterVelocity(gentity_t *ent, gvec3_t angles) { } } -void MS_Adjust(gclient_t *cl, mstats_t index, int count) { +static bool MS_Validation(gclient_t *cl, mstats_t index) { + if (!cl) + return false; + if (index <= MSTAT_NONE || index >= MSTAT_TOTAL) { gi.Com_PrintFmt("invalid match stat index: {}\n", index); - return; + return false; } if (!g_matchstats->integer || level.match_state != matchst_t::MATCH_IN_PROGRESS) + return false; + + if (cl->sess.is_a_bot) + return false; + + return true; +} + +int MS_Value(gclient_t *cl, mstats_t index) { + if (!MS_Validation(cl, index)) + return 0; + + return cl->resp.mstats[index]; +} + +void MS_Adjust(gclient_t *cl, mstats_t index, int count) { + if (!MS_Validation(cl, index)) return; cl->resp.mstats[index] += count; } + +void MS_AdjustDuo(gclient_t *cl, mstats_t index1, mstats_t index2, int count) { + if (!MS_Validation(cl, index1)) + return; + + cl->resp.mstats[index1] += count; + cl->resp.mstats[index2] += count; +} + +void MS_Set(gclient_t *cl, mstats_t index, int value) { + if (!MS_Validation(cl, index)) + return; + + cl->resp.mstats[index] = value; +} + +const char *stime() { + struct tm *ltime; + time_t gmtime; + + time(&gmtime); + ltime = localtime(&gmtime); + + const char *s; + s = G_Fmt("{}{:02}{:02}{:02}{:02}{:02}", + 1900 + ltime->tm_year, ltime->tm_mon + 1, ltime->tm_mday, ltime->tm_hour, ltime->tm_min, ltime->tm_sec + ).data(); + + return s; +} + +void AnnouncerSound(gentity_t *ent, const char *announcer_sound, const char *backup_sound, bool use_backup) { + for (auto ec : active_clients()) { + if (ent == world || ent == ec || (!ClientIsPlaying(ec->client) && ec->client->follow_target == ent)) { + if (ec->client->sess.is_a_bot) + continue; + if (!ec->client->sess.pc.use_expanded || (announcer_sound == nullptr && use_backup)) { + if (backup_sound) + gi.local_sound(ec, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(backup_sound), 1, ATTN_NONE, 0); + continue; + } + //gi.local_sound(ec, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(announcer_sound), 1, ATTN_NONE, 0); + + if (ec->client->sess.pc.use_expanded && announcer_sound) + gi.local_sound(ec, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(G_Fmt("vo_evil/{}.wav", announcer_sound).data()), 1, ATTN_NONE, 0); + } + } + //if (announcer_sound && ent == world) + // gi.positioned_sound(world->s.origin, world, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(G_Fmt("vo_evil/{}.wav", announcer_sound).data()), 1, ATTN_NONE, 0); +} + + +void QLSound(gentity_t *ent, const char *ql_sound, const char *backup_sound, bool use_backup) { + for (auto ec : active_clients()) { + if (ent == world || ent == ec || (!ClientIsPlaying(ec->client) && ec->client->follow_target == ent)) { + if (ec->client->sess.is_a_bot) + continue; + if (!ec->client->sess.pc.use_expanded || (ql_sound == nullptr && use_backup)) { + if (backup_sound) + gi.local_sound(ec, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(backup_sound), 1, ATTN_NONE, 0); + continue; + } + //gi.local_sound(ec, CHAN_AUTO | CHAN_RELIABLE, gi.soundindex(ql_sound), 1, ATTN_NONE, 0); + + if (ec->client->sess.pc.use_expanded && ql_sound) + gi.local_sound(ec, CHAN_RELIABLE | CHAN_NO_PHS_ADD | CHAN_AUX, gi.soundindex(G_Fmt("{}.wav", ql_sound).data()), 1, ATTN_NONE, 0); + } + } +} + +void G_StuffCmd(gentity_t *e, const char *fmt, ...) { + va_list argptr; + char text[512]; + + if (e && !e->client->pers.connected) + gi.Com_ErrorFmt("{}: Bad client %d for '%s'", __FUNCTION__, (int)(e - g_entities - 1), fmt); + + va_start(argptr, fmt); + vsnprintf(text, sizeof(text), fmt, argptr); + va_end(argptr); + text[sizeof(text) - 1] = 0; + + gi.WriteByte(svc_stufftext); + gi.WriteString(text); + + if (e) + gi.unicast(e, true); + else + gi.multicast(vec3_origin, MULTICAST_ALL, true); +} diff --git a/src/g_weapon.cpp b/src/g_weapon.cpp index 8f32517..4e39446 100644 --- a/src/g_weapon.cpp +++ b/src/g_weapon.cpp @@ -66,7 +66,7 @@ bool fire_hit(gentity_t *self, vec3_t aim, int damage, int kick) { if (!(tr.ent->svflags & SVF_MONSTER) && (!tr.ent->client)) return false; - MS_Adjust(self->owner->client, MSTAT_HITS, 1); + //MS_Adjust(self->owner->client, MSTAT_HITS, 1); // do our special form of knockback here v = (self->enemy->absmin + self->enemy->absmax) * 0.5f; @@ -353,6 +353,7 @@ TOUCH(blaster_touch) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool T_Damage(other, ent, ent->owner, ent->velocity, ent->s.origin, tr.plane.normal, ent->dmg, 1, DAMAGE_ENERGY | DAMAGE_STAT_ONCE, static_cast(ent->style)); MS_Adjust(ent->owner->client, MSTAT_HITS, 1); + //MS_Adjust(ent->owner->client, MSTAT_WP_BL_HITS, 1); } else { } @@ -449,6 +450,7 @@ static TOUCH(blaster2_touch) (gentity_t *self, gentity_t *other, const trace_t & self->owner->takedamage = damagestat; MS_Adjust(self->owner->client, MSTAT_HITS, 1); + //MS_Adjust(self->owner->client, MSTAT_WP_BL_HITS, 1); } else { if (self->dmg >= 5) T_RadiusDamage(self, self->owner, (float)(self->dmg * 2), other, self->splash_radius, DAMAGE_ENERGY, MOD_UNKNOWN); @@ -585,6 +587,7 @@ static THINK(Grenade_Explode) (gentity_t *ent) -> void { T_Damage(ent->enemy, ent, ent->owner, dir, ent->s.origin, vec3_origin, (int)points, (int)points, DAMAGE_RADIUS | DAMAGE_STAT_ONCE, mod); MS_Adjust(ent->owner->client, MSTAT_HITS, 1); + //MS_Adjust(ent->owner->client, (mod.id == MOD_HANDGRENADE) ? MSTAT_WP_HG_HITS : MSTAT_WP_GL_HITS, 1); } if (ent->spawnflags.has(SPAWNFLAG_GRENADE_HELD)) @@ -817,6 +820,7 @@ TOUCH(rocket_touch) (gentity_t *ent, gentity_t *other, const trace_t &tr, bool o T_Damage(other, ent, ent->owner, ent->velocity, ent->s.origin, tr.plane.normal, ent->dmg, RS(RS_MM) ? 50 : 0, DAMAGE_NONE | DAMAGE_STAT_ONCE, MOD_ROCKET); MS_Adjust(ent->owner->client, MSTAT_HITS, 1); + //MS_Adjust(ent->owner->client, MSTAT_WP_RL_HITS, 1); } else { // don't throw any debris in net games if (!deathmatch->integer && !coop->integer) { @@ -3055,11 +3059,11 @@ static THINK(Trap_Think) (gentity_t *ent) -> void { SP_item_foodcube(best); best->s.origin = ent->s.origin; best->s.origin[2] += 24 * best->s.scale; + best->s.old_origin = best->s.origin; best->s.angles[YAW] = frandom() * 360; best->velocity[2] = 400; best->think(best); best->nextthink = 0_ms; - best->s.old_origin = best->s.origin; gi.linkentity(best); gi.sound(best, CHAN_AUTO, gi.soundindex("misc/fhit3.wav"), 1.f, ATTN_NORM, 0.f); @@ -3112,8 +3116,6 @@ static THINK(Trap_Think) (gentity_t *ent) -> void { continue; if (target->health <= 0) continue; - if (target->client && target->client->eliminated) - continue; if (!visible(ent, target)) continue; vec = ent->s.origin - target->s.origin; diff --git a/src/game.h b/src/game.h index 34f82e3..f0f32e2 100644 --- a/src/game.h +++ b/src/game.h @@ -387,14 +387,18 @@ MAKE_ENUM_BITFLAGS(pmflags_t); struct pmove_state_t { pmtype_t pm_type; - vec3_t origin; - vec3_t velocity; - pmflags_t pm_flags; // ducked, jump_held, etc - uint16_t pm_time; - int16_t gravity; - gvec3_t delta_angles; // add to command angles to get view direction + vec3_t origin; + vec3_t velocity; + pmflags_t pm_flags; // ducked, jump_held, etc + uint16_t pm_time; + int16_t gravity; + gvec3_t delta_angles; // add to command angles to get view direction // changed by spawns, rotating objects, and teleporters - int8_t viewheight; // view height, added to origin[2] + viewoffset[2], for crouching + int8_t viewheight; // view height, added to origin[2] + viewoffset[2], for crouching + +//muffmode + bool haste; +//-muffmode }; // diff --git a/src/p_client.cpp b/src/p_client.cpp index 29bf0f4..31b2223 100644 --- a/src/p_client.cpp +++ b/src/p_client.cpp @@ -449,6 +449,9 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att case MOD_DOPPEL_EXPLODE: base = "$g_mod_self_dopple_explode"; break; + case MOD_EXPIRE: + base = "{0} ran out of blood.\n"; + break; default: base = "$g_mod_self_default"; break; @@ -635,9 +638,13 @@ static void ClientObituary(gentity_t *self, gentity_t *inflictor, gentity_t *att if (level.match_state == matchst_t::MATCH_WARMUP_READYUP) { BroadcastReadyReminderMessage(); } else if (attacker->client->resp.kill_count && !(attacker->client->resp.kill_count % 10)) { - gi.LocBroadcast_Print(PRINT_CENTER, "{} is on a {} spree\nwith {} frags!", attacker->client->resp.netname, GT(GT_FREEZE) ? "freezing" : "fragging", attacker->client->resp.kill_count); + gi.LocBroadcast_Print(PRINT_CENTER, "{} is on a rampage\nwith {} frags!", attacker->client->resp.netname, attacker->client->resp.kill_count); + AnnouncerSound(attacker, "rampage1", nullptr, false); + attacker->client->pers.medal_time = level.time; + attacker->client->pers.medal_type = MEDAL_RAMPAGE; + attacker->client->pers.medal_count[MEDAL_RAMPAGE]++; } else if (kill_count >= 10) { - gi.LocBroadcast_Print(PRINT_CENTER, "{} put an end to {}'s\n{} spree!", attacker->client->resp.netname, self->client->resp.netname, GT(GT_FREEZE) ? "freezing" : "fragging"); + gi.LocBroadcast_Print(PRINT_CENTER, "{} put an end to {}'s\nrampage!", attacker->client->resp.netname, self->client->resp.netname); } else if (Teams() || level.match_state != matchst_t::MATCH_IN_PROGRESS) { if (attacker->client->sess.pc.show_fragmessages) gi.LocClient_Print(attacker, PRINT_CENTER, "You {} {}", GT(GT_FREEZE) ? "froze" : "fragged", self->client->resp.netname); @@ -680,7 +687,7 @@ static void TossClientItems(gentity_t *self) { gitem_t *wp; gentity_t *drop; - bool quad, doubled, duelfire, protection, invis, regen; + bool quad, doubled, haste, protection, invis, regen; // drop weapon wp = self->client->pers.weapon; @@ -715,14 +722,14 @@ static void TossClientItems(gentity_t *self) { // drop powerup quad = g_dm_no_quad_drop->integer ? false : (self->client->pu_time_quad > (level.time + 1_sec)); - duelfire = g_dm_no_quadfire_drop->integer ? false : (self->client->pu_time_duelfire > (level.time + 1_sec)); + haste = (self->client->pu_time_haste > (level.time + 1_sec)); doubled = (self->client->pu_time_double > (level.time + 1_sec)); protection = (self->client->pu_time_protection > (level.time + 1_sec)); invis = (self->client->pu_time_invisibility > (level.time + 1_sec)); regen = (self->client->pu_time_regeneration > (level.time + 1_sec)); if (!g_dm_powerup_drop->integer) { - quad = doubled = duelfire = protection = invis = regen = false; + quad = doubled = haste = protection = invis = regen = false; } if (quad) { @@ -748,19 +755,19 @@ static void TossClientItems(gentity_t *self) { } } - if (duelfire) { + if (haste) { self->client->v_angle[YAW] += 45; - drop = Drop_Item(self, GetItemByIndex(IT_POWERUP_DUELFIRE)); + drop = Drop_Item(self, GetItemByIndex(IT_POWERUP_HASTE)); drop->spawnflags |= SPAWNFLAG_ITEM_DROPPED_PLAYER; drop->spawnflags &= ~SPAWNFLAG_ITEM_DROPPED; drop->svflags &= ~SVF_INSTANCED; drop->touch = Touch_Item; - drop->nextthink = self->client->pu_time_duelfire; + drop->nextthink = self->client->pu_time_haste; drop->think = G_FreeEntity; // decide how many seconds it has left - drop->count = self->client->pu_time_duelfire.seconds() - level.time.seconds(); + drop->count = self->client->pu_time_haste.seconds() - level.time.seconds(); if (drop->count < 1) { drop->count = 1; } @@ -917,7 +924,7 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int self->maxs[2] = -8; - if (attacker && attacker->client && level.match_state == matchst_t::MATCH_IN_PROGRESS) { + if (attacker && attacker->client && level.match_state == matchst_t::MATCH_IN_PROGRESS && notGT(GT_RACE)) { if (attacker == self || mod.friendly_fire) { if (!mod.no_point_loss) G_AdjustPlayerScore(attacker->client, -1, GT(GT_TDM), -1); @@ -927,13 +934,34 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int if (attacker->health > 0) attacker->client->resp.kill_count++; - MS_Adjust(attacker->client, MSTAT_KILLS, 1); + MS_Adjust(attacker->client, MSTAT_KILLS_TOTAL, 1); + if (1_sec > (level.time - self->client->respawn_time)) + MS_Adjust(attacker->client, MSTAT_KILLS_SPAWN, 1); + + if (attacker->client->pers.kill_time && (attacker->client->pers.kill_time + 2_sec > level.time)) { + attacker->client->pers.medal_time = level.time; + attacker->client->pers.medal_type = MEDAL_EXCELLENT; + attacker->client->pers.medal_count[MEDAL_EXCELLENT]++; + + if (attacker->client->pers.medal_count[MEDAL_EXCELLENT] == 1) + AnnouncerSound(attacker, "first_excellent", nullptr, false); + else + AnnouncerSound(attacker, "excellent1", nullptr, false); + } + attacker->client->pers.kill_time = level.time; + + if (mod.id == MOD_BLASTER || mod.id == MOD_CHAINFIST) { + attacker->client->pers.medal_time = level.time; + attacker->client->pers.medal_type = MEDAL_HUMILIATION; + attacker->client->pers.medal_count[MEDAL_HUMILIATION]++; + + AnnouncerSound(attacker, "humiliation1", nullptr, false); + } for (auto ec : active_clients()) { if (!ClientIsPlaying(ec->client) && ec->client->sess.pc.follow_killer) { - ec->client->follow_target = attacker; - ec->client->follow_update = true; - UpdateChaseCam(ec); + ec->client->follow_queued_target = attacker; + ec->client->follow_queued_time = level.time; } } } @@ -941,16 +969,27 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int if (!mod.no_point_loss) G_AdjustPlayerScore(self->client, -1, GT(GT_TDM), -1); } - MS_Adjust(self->client, MSTAT_DEATHS, 1); + MS_Adjust(self->client, MSTAT_DEATHS_TOTAL, 1); + + if (self == attacker) + MS_Adjust(self->client, MSTAT_DEATHS_SUICIDES, 1); + else if (!attacker) + MS_Adjust(self->client, MSTAT_DEATHS_ENVIRO, 1); + else if (1_sec > (level.time - self->client->respawn_time)) + MS_Adjust(self->client, MSTAT_DEATHS_SPAWN, 1); self->svflags |= SVF_DEADMONSTER; if (!self->deadflag) { self->client->respawn_time = (level.time + 1_sec); - self->client->respawn_min_time = (level.time + gtime_t::from_sec(g_dm_respawn_delay_min->value)); - if (deathmatch->integer && g_dm_force_respawn_time->integer) { - self->client->respawn_time = (level.time + gtime_t::from_sec(g_dm_force_respawn_time->value)); + if (deathmatch->integer && g_gametype->integer == GT_RACE) { + self->client->respawn_min_time = self->client->respawn_time = level.time; + } else { + self->client->respawn_min_time = (level.time + gtime_t::from_sec(g_dm_respawn_delay_min->value)); + if (deathmatch->integer && g_dm_force_respawn_time->integer) { + self->client->respawn_time = (level.time + gtime_t::from_sec(g_dm_force_respawn_time->value)); + } } LookAtKiller(self, inflictor, attacker); @@ -977,7 +1016,7 @@ DIE(player_die) (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int // remove powerups self->client->pu_time_quad = 0_ms; - self->client->pu_time_duelfire = 0_ms; + self->client->pu_time_haste = 0_ms; self->client->pu_time_double = 0_ms; self->client->pu_time_protection = 0_ms; self->client->pu_time_invisibility = 0_ms; @@ -1183,6 +1222,12 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.health = 100; client->pers.max_health = 100; + client->pers.medal_time = 0_sec; + client->pers.medal_type = MEDAL_NONE; + client->pers.medal_count[MEDAL_EXCELLENT] = 0; + client->pers.medal_count[MEDAL_HUMILIATION] = 0; + client->pers.medal_count[MEDAL_IMPRESSIVE] = 0; + // don't give us weapons if we shouldn't have any if (ClientIsPlaying(client)) { // in coop, if there's already a player in the game and we're new, @@ -1356,7 +1401,9 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { if (level.start_items && *level.start_items) Player_GiveStartItems(ent, level.start_items); - if (!deathmatch->integer || level.match_state < matchst_t::MATCH_IN_PROGRESS) + if (deathmatch->integer && GT(GT_RACE)) + client->pers.inventory[IT_COMPASS] = 1; + else if (!deathmatch->integer || level.match_state < matchst_t::MATCH_IN_PROGRESS) // compass also used for ready status toggling in deathmatch client->pers.inventory[IT_COMPASS] = 1; @@ -1386,25 +1433,26 @@ void InitClientPersistant(gentity_t *ent, gclient_t *client) { client->pers.spawned = true; } -static void InitClientResp(gclient_t *client) { - bool showed_help = client->resp.showed_help; - team_t team = client->sess.team; +static void InitClientResp(gclient_t *cl) { + bool showed_help = cl->resp.showed_help; + team_t team = cl->sess.team; + int motd_mod_count = cl->resp.motd_mod_count; char netname[MAX_NETNAME]; - Q_strlcpy(netname, client->resp.netname, sizeof(netname)); + Q_strlcpy(netname, cl->resp.netname, sizeof(netname)); - memset(&client->resp, 0, sizeof(client->resp)); + memset(&cl->resp, 0, sizeof(cl->resp)); - client->resp.showed_help = showed_help; + cl->resp.showed_help = showed_help; - Q_strlcpy(client->resp.netname, netname, sizeof(client->resp.netname)); + Q_strlcpy(cl->resp.netname, netname, sizeof(cl->resp.netname)); - client->resp.entertime = level.time; - client->resp.coop_respawn = client->pers; - - client->resp.kill_count = 0; + cl->resp.entertime = level.time; + cl->resp.coop_respawn = cl->pers; + + cl->resp.motd_mod_count = motd_mod_count; - client->sess.team = team; + cl->sess.team = team; } /* @@ -2061,12 +2109,17 @@ bool SelectSpawnPoint(gentity_t *ent, vec3_t &origin, vec3_t &angles, bool force if (Teams() && ClientIsPlaying(ent->client)) spot = SelectTeamSpawnPoint(ent, force_spawn); else { - select_spawn_result_t result = SelectDeathmatchSpawnPoint(ent, ent->client->spawn_origin, (playerspawn_t)clamp(g_dm_spawn_farthest->integer, 0, 3), force_spawn, true, !ClientIsPlaying(ent->client) || ent->client->eliminated, false); + if (g_gametype->integer == GT_RACE) + spot = SelectSingleSpawnPoint(ent); - if (!result.any_valid) - gi.Com_Error("No valid spawn points found."); + if (!spot) { + select_spawn_result_t result = SelectDeathmatchSpawnPoint(ent, ent->client->spawn_origin, (playerspawn_t)clamp(g_dm_spawn_farthest->integer, 0, 3), force_spawn, true, !ClientIsPlaying(ent->client) || ent->client->eliminated, false); - spot = result.spot; + if (!result.any_valid) + gi.Com_Error("No valid spawn points found."); + + spot = result.spot; + } } if (spot) { @@ -2537,9 +2590,9 @@ static bool InitPlayerTeam(gentity_t *ent) { ent->client->sess.team = TEAM_SPECTATOR; MoveClientToFreeCam(ent); - - if (!(level.match_state == matchst_t::MATCH_IN_PROGRESS && g_match_lock->integer)) { - if (ent->svflags & SVF_BOT || g_dm_force_join->integer || g_dm_auto_join->integer) { + + if (level.match_state < matchst_t::MATCH_COUNTDOWN || (level.match_state >= matchst_t::MATCH_COUNTDOWN && !g_match_lock->integer)) { + if (ent->client->sess.is_a_bot || (ent->svflags & SVF_BOT) || g_dm_force_join->integer || g_dm_auto_join->integer) { if (ent != &g_entities[1] || (ent == &g_entities[1] && g_owner_auto_join->integer)) { SetTeam(ent, PickTeam(-1), false, false, false); return true; @@ -2758,6 +2811,7 @@ void ClientSpawn(gentity_t *ent) { ent->flags &= ~(FL_NO_KNOCKBACK | FL_ALIVE_KNOCKBACK_ONLY | FL_NO_DAMAGE_EFFECTS | FL_SAM_RAIMI); ent->svflags &= ~SVF_DEADMONSTER; ent->svflags |= SVF_PLAYER; + ent->client->pers.last_spawn_time = level.time; ent->mins = PLAYER_MINS; ent->maxs = PLAYER_MAXS; @@ -3447,6 +3501,7 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); + G_StuffCmd(ent, "disconnect\n"); return true; } @@ -3469,6 +3524,7 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); + G_StuffCmd(ent, "disconnect\n"); return true; } @@ -3490,9 +3546,9 @@ static inline bool CheckBanned(gentity_t *ent, char *userinfo, const char *socia gi.LocBroadcast_Print(PRINT_CHAT, "{}: bejesus, what a lovely lobby! certainly better than 888's!\n", name); } } - gi.local_sound(ent, CHAN_AUTO, gi.soundindex("world/klaxon3.wav"), 1, ATTN_NONE, 0); gi.AddCommandString(G_Fmt("kick {}\n", ent - g_entities - 1).data()); + G_StuffCmd(ent, "disconnect\n"); return true; } return false; @@ -3524,7 +3580,7 @@ bool ClientConnect(gentity_t *ent, char *userinfo, const char *social_id, bool i if (!is_bot && CheckBanned(ent, userinfo, social_id)) return false; - //ent->client->sess.team = deathmatch->integer ? TEAM_SPECTATOR : TEAM_FREE; + ent->client->sess.team = deathmatch->integer ? TEAM_NONE : TEAM_FREE; // they can connect ent->client = game.clients + (ent - g_entities - 1); @@ -3540,8 +3596,8 @@ bool ClientConnect(gentity_t *ent, char *userinfo, const char *social_id, bool i if (!ent->client->sess.initialised && !ent->client->sess.team) { //gi.Com_PrintFmt_("ClientConnect: {} q={}\n", ent->client->resp.netname, ent->client->sess.duel_queued); // force team join - //ent->client->sess.team = deathmatch->integer ? TEAM_NONE : TEAM_FREE; - InitPlayerTeam(ent); + ent->client->sess.team = deathmatch->integer ? TEAM_SPECTATOR : TEAM_FREE; + //InitPlayerTeam(ent); ent->client->sess.pc.show_id = true; ent->client->sess.pc.show_timer = true; ent->client->sess.pc.show_fragmessages = true; @@ -3879,13 +3935,20 @@ Actions that happen once a second ================== */ static void ClientTimerActions(gentity_t *ent) { - // currently only used for health/armor countdown in Q3A ruleset - if (!(RS(RS_Q3A))) + if (ent->client->time_residual > level.time) return; - if (level.time >= ent->client->time_residual) { - ent->client->time_residual = level.time + 1_sec; + MS_Adjust(ent->client, MSTAT_HEALTH_TRACKER, ent->health); + MS_Adjust(ent->client, MSTAT_HEALTH_TICKS, 1); + if (ent->health > MS_Value(ent->client, MSTAT_HEALTH_PEAK)) + MS_Set(ent->client, MSTAT_HEALTH_PEAK, ent->health); + MS_Adjust(ent->client, MSTAT_PING_TRACKER, ent->client->ping); + MS_Adjust(ent->client, MSTAT_PING_TICKS, 1); + if (ent->client->ping > MS_Value(ent->client, MSTAT_PING_PEAK)) + MS_Set(ent->client, MSTAT_HEALTH_PEAK, ent->client->ping); + + if (RS(RS_Q3A)) { // count down health when over max if (ent->health > ent->client->pers.max_health) ent->health--; @@ -3894,6 +3957,8 @@ static void ClientTimerActions(gentity_t *ent) { if (ent->client->pers.inventory[IT_ARMOR_COMBAT] > ent->client->pers.max_health) ent->client->pers.inventory[IT_ARMOR_COMBAT]--; } + + ent->client->time_residual = level.time + 1_sec; } /* @@ -3941,6 +4006,17 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { client->initial_menu_shown = true; } } + + // check for queued follow targets + if (!ClientIsPlaying(client)) { + if (client->follow_queued_target && level.time > client->follow_queued_time + 500_ms) { + client->follow_target = client->follow_queued_target; + client->follow_update = true; + client->follow_queued_target = nullptr; + client->follow_queued_time = 0_sec; + UpdateChaseCam(ent); + } + } // check for inactivity timer if (!ClientInactivityTimer(ent)) @@ -3964,12 +4040,12 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { if (ent->client->sess.team_join_time) { gtime_t delay = 5_sec; - if (ent->client->resp.motd_mod_count != game.motd_modcount) { + if (ent->client->resp.motd_mod_count != game.motd_mod_count) { if (level.time >= ent->client->sess.team_join_time + delay) { if (g_showmotd->integer && game.motd.size()) { gi.LocCenter_Print(ent, "{}", game.motd.c_str()); delay += 5_sec; - ent->client->resp.motd_mod_count = game.motd_modcount; + ent->client->resp.motd_mod_count = game.motd_mod_count; } } } @@ -4064,6 +4140,9 @@ void ClientThink(gentity_t *ent, usercmd_t *ucmd) { else client->ps.pmove.pm_flags &= ~PMF_IGNORE_PLAYER_COLLISION; + // haste support + client->ps.pmove.haste = client->pu_time_haste > level.time; + // trigger_gravity support client->ps.pmove.gravity = (short)(level.gravity * ent->gravity); pm.s = client->ps.pmove; diff --git a/src/p_hud.cpp b/src/p_hud.cpp index a0e1e97..ef229bb 100644 --- a/src/p_hud.cpp +++ b/src/p_hud.cpp @@ -50,7 +50,7 @@ void MoveClientToIntermission(gentity_t *ent) { ent->client->pu_time_enviro = 0_ms; ent->client->pu_time_invisibility = 0_ms; ent->client->pu_time_regeneration = 0_ms; - ent->client->pu_time_duelfire = 0_ms; + ent->client->pu_time_haste = 0_ms; ent->client->pu_time_double = 0_ms; ent->client->grenade_blew_up = false; @@ -599,7 +599,7 @@ static void DuelScoreboardMessage(gentity_t *ent, gentity_t *killer) { fmt::format_to(std::back_inserter(entry), FMT_STRING("client {} {} {} {} {} {} "), - x, y, level.sorted_clients[i], cl->resp.score, cl->ping, (level.time - cl->sess.team_join_time).minutes()); + x, y, level.sorted_clients[i], cl->resp.score, cl->ping, GT(GT_RACE) ? cl->resp.score : 0); // (level.time - cl->sess.team_join_time).minutes()); if (string.length() + entry.length() > MAX_STRING_CHARS) break; @@ -862,9 +862,10 @@ void DeathmatchScoreboardMessage(gentity_t *ent, gentity_t *killer) { 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())); } else if (level.match_state == MATCH_IN_PROGRESS) { + const char *score = (g_gametype->integer == GT_RACE) ? G_TimeStringMs(ent->client->resp.score, false) : G_Fmt("{}", ent->client->resp.score).data(); if (ent->client && ClientIsPlaying(ent->client) && ent->client->resp.score && level.num_playing_clients > 1) { fmt::format_to(std::back_inserter(string), FMT_STRING("xv 0 yv -10 cstring2 \"{} place with a score of {}\" "), - G_PlaceString(ent->client->resp.rank + 1), ent->client->resp.score); + G_PlaceString(ent->client->resp.rank + 1), score); } //if (fraglimit->integer && !(GTF(GTF_ROUNDS))) // fmt::format_to(std::back_inserter(string), FMT_STRING("xv -20 yv -10 loc_string2 1 $g_score_frags \"{}\" "), fraglimit->integer); @@ -1099,7 +1100,7 @@ struct powerup_info_t { int32_t gclient_t:: *count_ptr = nullptr; } powerup_table[] = { { IT_POWERUP_QUAD, &gclient_t::pu_time_quad }, - { IT_POWERUP_DUELFIRE, &gclient_t::pu_time_duelfire }, + { IT_POWERUP_HASTE, &gclient_t::pu_time_haste }, { IT_POWERUP_DOUBLE, &gclient_t::pu_time_double }, { IT_POWERUP_PROTECTION, &gclient_t::pu_time_protection }, { IT_POWERUP_INVISIBILITY, &gclient_t::pu_time_invisibility }, @@ -1845,7 +1846,11 @@ void G_SetStats(gentity_t *ent) { break; case matchst_t::MATCH_WARMUP_DEFAULT: case matchst_t::MATCH_WARMUP_READYUP: - s1 = "WARMUP"; + if (GT(GT_RACE)) { + gtime_t t2 = (level.time - ent->client->pers.last_spawn_time); + s1 = G_Fmt("WARMUP ({})", G_TimeStringMs(t2.milliseconds(), false)).data(); + } else + s1 = "WARMUP"; break; case matchst_t::MATCH_COUNTDOWN: s1 = "COUNTDOWN"; @@ -1853,7 +1858,7 @@ void G_SetStats(gentity_t *ent) { default: { if (level.timeout_in_place > 0_ms) { int t2 = (level.timeout_in_place).milliseconds(); - s1 = G_Fmt("TIMEOUT! ({})", G_TimeString(t, false)).data(); + s1 = G_Fmt("TIMEOUT! ({})", G_TimeString(t2, false)).data(); } else if (t < 0 && t >= -4) { s1 = "OVERTIME!"; } else if (GTF(GTF_ROUNDS)) { @@ -1866,7 +1871,11 @@ void G_SetStats(gentity_t *ent) { s1 = ""; } } else { - s1 = G_TimeString(t, false); + if (GT(GT_RACE)) { + gtime_t t2 = (level.time - ent->client->pers.last_spawn_time); + s1 = G_Fmt("{} ({})", G_TimeString(t, false), G_TimeStringMs(t2.milliseconds(), false)).data(); + } else + s1 = G_TimeString(t, false); } break; } @@ -1895,6 +1904,12 @@ void G_SetStats(gentity_t *ent) { } else { ent->client->ps.stats[STAT_MATCH_STATE] = 0; } + + if (ent->client->pers.medal_time + 3_sec > level.time) { + ent->client->ps.stats[STAT_CHASE] = 0; // CONFIG_TEAMINFO; + } else { + ent->client->ps.stats[STAT_CHASE] = 0; + } } diff --git a/src/p_move.cpp b/src/p_move.cpp index 3d969a9..76b27bb 100644 --- a/src/p_move.cpp +++ b/src/p_move.cpp @@ -184,6 +184,10 @@ float pm_laddermod = 0.5f; */ +static float MaxSpeed(pmove_state_t *ps) { + return ps->haste ? pm_maxspeed * 1.3 : pm_maxspeed; +} + /* ================== PM_ClipVelocity @@ -595,7 +599,7 @@ static void PM_AddCurrents(vec3_t &wishvel) { if (pm->s.pm_flags & PMF_ON_LADDER) { if (pm->cmd.buttons & (BUTTON_JUMP | BUTTON_CROUCH)) { // [Paril-KEX]: if we're underwater, use full speed on ladders - float ladder_speed = pm->waterlevel >= WATER_WAIST ? pm_maxspeed : 200; + float ladder_speed = pm->waterlevel >= WATER_WAIST ? MaxSpeed(&pm->s) : 200; if (pm->cmd.buttons & BUTTON_JUMP) wishvel[2] = ladder_speed; @@ -727,6 +731,7 @@ static void PM_WaterMove() { vec3_t wishvel; float wishspeed; vec3_t wishdir; + float maxspeed = MaxSpeed(&pm->s); // // user intentions @@ -750,9 +755,9 @@ static void PM_WaterMove() { wishdir = wishvel; wishspeed = wishdir.normalize(); - if (wishspeed > pm_maxspeed) { - wishvel *= pm_maxspeed / wishspeed; - wishspeed = pm_maxspeed; + if (wishspeed > maxspeed) { + wishvel *= maxspeed / wishspeed; + wishspeed = maxspeed; } wishspeed *= 0.5f; @@ -795,7 +800,7 @@ static void PM_AirMove() { // // clamp to server defined max speed // - maxspeed = (pm->s.pm_flags & PMF_DUCKED) ? pm_duckspeed : pm_maxspeed; + maxspeed = (pm->s.pm_flags & PMF_DUCKED) ? pm_duckspeed : MaxSpeed(&pm->s); if (wishspeed > maxspeed) { wishvel *= maxspeed / wishspeed; @@ -1117,6 +1122,7 @@ static void PM_FlyMove(bool doclip) { float fmove, smove; vec3_t wishdir; float wishspeed; + float maxspeed = MaxSpeed(&pm->s); pm->s.viewheight = doclip ? 0 : 22; @@ -1162,9 +1168,9 @@ static void PM_FlyMove(bool doclip) { // // clamp to server defined max speed // - if (wishspeed > pm_maxspeed) { - wishvel *= pm_maxspeed / wishspeed; - wishspeed = pm_maxspeed; + if (wishspeed > maxspeed) { + wishvel *= maxspeed / wishspeed; + wishspeed = maxspeed; } // Paril: newer clients do this diff --git a/src/p_view.cpp b/src/p_view.cpp index ebe00a4..b5ad66e 100644 --- a/src/p_view.cpp +++ b/src/p_view.cpp @@ -527,8 +527,8 @@ static void G_CalcBlend(gentity_t *ent) { gi.sound(ent, CHAN_ITEM, gi.soundindex("items/damage2.wav"), 1, ATTN_NORM, 0); if (G_PowerUpExpiringRelative(remaining)) G_AddBlend(0, 0, 1, 0.08f, ent->client->ps.screen_blend); - } else if (ent->client->pu_time_duelfire > level.time) { - remaining = ent->client->pu_time_duelfire - level.time; + } else if (ent->client->pu_time_haste > level.time) { + remaining = ent->client->pu_time_haste - level.time; if (remaining.milliseconds() == 3000) // beginning to fade gi.sound(ent, CHAN_ITEM, gi.soundindex("items/quadfire2.wav"), 1, ATTN_NORM, 0); if (G_PowerUpExpiringRelative(remaining)) @@ -823,8 +823,8 @@ static void G_SetClientEffects(gentity_t *ent) { if (ent->client->pu_time_protection > level.time) if (G_PowerUpExpiring(ent->client->pu_time_protection)) ent->s.effects |= EF_PENT; - if (ent->client->pu_time_duelfire > level.time) - if (G_PowerUpExpiring(ent->client->pu_time_duelfire)) + if (ent->client->pu_time_haste > level.time) + if (G_PowerUpExpiring(ent->client->pu_time_haste)) ent->s.effects |= EF_DUALFIRE; if (ent->client->pu_time_double > level.time) if (G_PowerUpExpiring(ent->client->pu_time_double)) @@ -1147,7 +1147,7 @@ void Frenzy_ApplyAmmoRegen(gentity_t *ent) { if (!g_frenzy->integer) return; - if (g_infinite_ammo->integer || g_instagib->integer || g_nadefest->integer) + if (InfiniteAmmoOn(nullptr)) return; client = ent->client; @@ -1273,7 +1273,7 @@ void ClientEndServerFrame(gentity_t *ent) { // vampiric damage expiration // don't expire if only 1 player in the match - if (g_vampiric_damage->integer && ClientIsPlaying(ent->client) && !ent->client->ps.stats[STAT_CHASE] && !level.intermission_time && g_vampiric_exp_min->integer && ent->health > g_vampiric_exp_min->integer) { + if (g_vampiric_damage->integer && ClientIsPlaying(ent->client) && !IsCombatDisabled() && (ent->health > g_vampiric_exp_min->integer)) { if (level.num_playing_clients > 1 && level.time > ent->client->vampire_expiretime) { int quantity = floor((ent->health - 1) / ent->max_health) + 1; ent->health -= quantity; @@ -1400,9 +1400,17 @@ void ClientEndServerFrame(gentity_t *ent) { G_CalcBlend(e); // chase cam stuff - if (!ClientIsPlaying(ent->client) || ent->client->eliminated) + if (!ClientIsPlaying(ent->client) || ent->client->eliminated) { G_SetSpectatorStats(ent); - else + + if (ent->client->follow_target) { + ent->client->ps.screen_blend = ent->client->follow_target->client->ps.screen_blend; + ent->client->ps.damage_blend = ent->client->follow_target->client->ps.damage_blend; + + ent->s.effects = ent->client->follow_target->s.effects; + ent->s.renderfx = ent->client->follow_target->s.renderfx; + } + } else G_SetStats(ent); G_CheckChaseStats(ent); diff --git a/src/p_weapon.cpp b/src/p_weapon.cpp index 7ab3e81..eea8905 100644 --- a/src/p_weapon.cpp +++ b/src/p_weapon.cpp @@ -6,7 +6,7 @@ #include "monsters/m_player.h" bool is_quad; -bool is_quadfire; +bool is_haste; player_muzzle_t is_silenced; byte damage_multiplier; @@ -19,7 +19,7 @@ bool InfiniteAmmoOn(gitem_t *item) { if (item && item->flags & IF_NO_INFINITE_AMMO) return false; - return g_infinite_ammo->integer || (deathmatch->integer && (g_instagib->integer || g_nadefest->integer)); + return g_infinite_ammo->integer || (deathmatch->integer && (g_instagib->integer || g_nadefest->integer || g_gametype->integer == GT_RACE)); } /* @@ -329,7 +329,7 @@ static void Weapon_RunThink(gentity_t *ent) { P_DamageModifier(ent); - is_quadfire = (ent->client->pu_time_duelfire > level.time); + is_haste = (ent->client->pu_time_haste > level.time); if (ent->client->silencer_shots) is_silenced = MZ_SILENCED; @@ -493,8 +493,8 @@ static inline gtime_t Weapon_AnimationTime(gentity_t *ent) { ent->client->ps.gunrate = 10; if (ent->client->ps.gunframe != 0 && (!(ent->client->pers.weapon->flags & IF_NO_HASTE) || ent->client->weaponstate != WEAPON_FIRING)) { - if (is_quadfire) - ent->client->ps.gunrate *= 2; + if (is_haste) + ent->client->ps.gunrate *= 1.5; if (Tech_ApplyTimeAccel(ent)) ent->client->ps.gunrate *= 2; if (g_frenzy->integer) @@ -711,8 +711,8 @@ void Weapon_PowerupSound(gentity_t *ent) { gi.sound(ent, CHAN_ITEM, gi.soundindex("items/damage3.wav"), 1, ATTN_NORM, 0); else if (ent->client->pu_time_double > level.time) gi.sound(ent, CHAN_ITEM, gi.soundindex("misc/ddamage3.wav"), 1, ATTN_NORM, 0); - else if (ent->client->pu_time_duelfire > level.time - && ent->client->tech_sound_time < level.time) { + else if (ent->client->pu_time_haste > level.time + && ent->client->tech_sound_time < level.time) { ent->client->tech_sound_time = level.time + 1_sec; gi.sound(ent, CHAN_ITEM, gi.soundindex("ctf/tech3.wav"), 1, ATTN_NORM, 0); } @@ -1148,7 +1148,7 @@ void Throw_Generic(gentity_t *ent, int FRAME_FIRE_LAST, int FRAME_IDLE_LAST, int if (Tech_ApplyTimeAccel(ent)) grenade_wait_time *= 0.5f; - if (is_quadfire) + if (is_haste) grenade_wait_time *= 0.5f; if (g_frenzy->integer) grenade_wait_time *= 0.5f;