From a701ca76d41182d34003e330a26698069276d20b Mon Sep 17 00:00:00 2001 From: terencehill Date: Thu, 4 Jan 2024 17:11:53 +0100 Subject: [PATCH] Freeze Tag: implement lives: a player now can get frozen only 2 times (configurable with g_freezetag_frozen_lives) in a round before getting eliminated; on freeze a centerprint message shows the number of lives remaining. --- gamemodes-server.cfg | 2 + notifications.cfg | 1 + .../gamemode/freezetag/sv_freezetag.qc | 259 ++++++++++++++++-- qcsrc/common/notifications/all.inc | 1 + qcsrc/server/world.qc | 1 + 5 files changed, 236 insertions(+), 28 deletions(-) diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index 01d297e08..867b941f0 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -418,6 +418,8 @@ set g_freezetag_revive_auto_reducible 1 "reduce auto-revival time when frozen pl set g_freezetag_revive_auto_reducible_forcefactor 0.01 "hit force to time reduction conversion factor" set g_freezetag_revive_auto_reducible_maxforce 400 "max force considered at once" set g_freezetag_revive_spawnshield 1 "apply spawnshield for this time in seconds after the player has been revived" +set g_freezetag_spectate_enemies 0 "allow eliminated players to spectate enemy players during Freeze Tag games. -1 blocks freeroam camera. Changes to this cvar take effect from the next round" +set g_freezetag_frozen_lives 2 "number of times a player can get frozen before getting eliminated. If this number is <= 0 (or >= 100) players will never get eliminated (inifinite lives)" set g_freezetag_frozen_maxtime 60 "frozen players will be automatically unfrozen after this time in seconds" set g_freezetag_teams_override 0 set g_freezetag_team_spawns 0 "when 1, players spawn from the team spawnpoints of the map, if any" diff --git a/notifications.cfg b/notifications.cfg index 22a4e70a3..d8574a405 100644 --- a/notifications.cfg +++ b/notifications.cfg @@ -458,6 +458,7 @@ seta notification_CENTER_DOOR_LOCKED_NEED "1" "0 = off, 1 = centerprint" seta notification_CENTER_DOOR_UNLOCKED "1" "0 = off, 1 = centerprint" seta notification_CENTER_EXTRALIVES "1" "0 = off, 1 = centerprint" seta notification_CENTER_FREEZETAG_AUTO_REVIVED "1" "0 = off, 1 = centerprint" +seta notification_CENTER_FREEZETAG_LIVES_REMAINING "1" "0 = off, 1 = centerprint" seta notification_CENTER_FREEZETAG_REVIVE "1" "0 = off, 1 = centerprint" seta notification_CENTER_FREEZETAG_REVIVED "1" "0 = off, 1 = centerprint" seta notification_CENTER_FREEZETAG_REVIVE_SELF "1" "0 = off, 1 = centerprint" diff --git a/qcsrc/common/gamemodes/gamemode/freezetag/sv_freezetag.qc b/qcsrc/common/gamemodes/gamemode/freezetag/sv_freezetag.qc index 3f7fff9eb..15e33daec 100644 --- a/qcsrc/common/gamemodes/gamemode/freezetag/sv_freezetag.qc +++ b/qcsrc/common/gamemodes/gamemode/freezetag/sv_freezetag.qc @@ -3,9 +3,11 @@ #include #include +int autocvar_g_freezetag_frozen_lives = 2; float autocvar_g_freezetag_frozen_maxtime; float autocvar_g_freezetag_revive_clearspeed; float autocvar_g_freezetag_round_timelimit; +int autocvar_g_freezetag_spectate_enemies; //int autocvar_g_freezetag_teams; int autocvar_g_freezetag_teams_override; float autocvar_g_freezetag_warmup; @@ -19,6 +21,19 @@ float autocvar_g_ft_start_ammo_cells = 180; float autocvar_g_ft_start_ammo_plasma = 180; float autocvar_g_ft_start_ammo_fuel = 0; +.int lives; +const int FT_INFINITE_LIVES = 100; // this number appears in the description of g_freezetag_frozen_lives too +bool g_freezetag_spectate_enemies; // updated on map reset + +int freezetag_Get_Start_Lives() +{ + int start_lives = bound(0, autocvar_g_freezetag_frozen_lives, FT_INFINITE_LIVES); + // 0 lives is not acceptable because players would die without getting frozen even once + if (!autocvar_g_freezetag_frozen_lives || warmup_stage) + start_lives = FT_INFINITE_LIVES; + return start_lives; +} + void freezetag_count_alive_players() { total_players = 0; @@ -197,6 +212,8 @@ bool freezetag_isEliminated(entity e) { if(IS_PLAYER(e) && (STAT(FROZEN, e) == FROZEN_NORMAL || IS_DEAD(e))) return true; + if(INGAME_JOINED(e) && e.frags == FRAGS_PLAYER_OUT_OF_GAME) + return true; return false; } @@ -315,6 +332,8 @@ void ft_RemovePlayer(entity this) freezetag_LastPlayerForTeam_Notify(this); Unfreeze(this, false); + this.lives = 0; + INGAME_STATUS_CLEAR(this); SetResourceExplicit(this, RES_HEALTH, 0); // neccessary to correctly count alive players freezetag_count_alive_players(); } @@ -331,7 +350,29 @@ MUTATOR_HOOKFUNCTION(ft, MakePlayerObserver) { entity player = M_ARGV(0, entity); - ft_RemovePlayer(player); + bool is_forced = M_ARGV(1, bool); + if (is_forced && INGAME(player)) + { + INGAME_STATUS_CLEAR(player); + ft_RemovePlayer(player); + } + + if (player.killindicator_teamchange == -2) // player wants to spectate + { + entcs_update_players(player); + INGAME_STATUS_CLEAR(player); + ft_RemovePlayer(player); + } + if (INGAME(player)) + { + player.frags = FRAGS_PLAYER_OUT_OF_GAME; + player.would_spectate = observe_blocked_if_eliminated; // if blocked from observing force to spectate now + } + if (!warmup_stage) + eliminatedPlayers.SendFlags |= 1; + if (!INGAME(player)) + return false; // allow team reset + return true; // prevent team reset } MUTATOR_HOOKFUNCTION(ft, PlayerDies) @@ -359,8 +400,19 @@ MUTATOR_HOOKFUNCTION(ft, PlayerDies) // if you succeed changing team through the menu if (frag_deathtype == DEATH_TEAMCHANGE.m_id || frag_deathtype == DEATH_AUTOTEAMCHANGE.m_id) { - freezetag_Add_Score(frag_target, frag_attacker); freezetag_count_alive_players(); + // anticipate Unfreeze call here otherwise when it gets called later it will reset + // freezetag_frozen_timeout to a value > 0 and player won't get frozen on respawn + if (STAT(FROZEN, frag_target) == FROZEN_NORMAL) + Unfreeze(frag_target, true); + else if (!warmup_stage) + { + if (frag_target.lives < FT_INFINITE_LIVES) + { + frag_target.lives--; + Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_LIVES_REMAINING, frag_target.lives); + } + } freezetag_LastPlayerForTeam_Notify(frag_target); frag_target.freezetag_frozen_timeout = -2; // freeze on respawn return true; @@ -373,45 +425,88 @@ MUTATOR_HOOKFUNCTION(ft, PlayerDies) if ((STAT(FROZEN, frag_target) != FROZEN_NORMAL) && !IS_PLAYER(frag_attacker)) GameRules_scoring_add(frag_target, SCORE, -1); - // by restoring some health right after player death (soft-kill) - // weapons and ammo won't be reset - SetResourceExplicit(frag_target, RES_HEALTH, 1); - // restore armor as it was removed in PlayerDamage - SetResourceExplicit(frag_target, RES_ARMOR, frag_target.freezetag_frozen_armor); - - // relocate - entity spot = SelectSpawnPoint(frag_target, true); - setorigin(frag_target, spot.origin); - frag_target.oldorigin = frag_target.origin; - frag_target.fixangle = true; // turn this way immediately - frag_target.angles = vec2(spot.angles); - frag_target.velocity = '0 0 0'; - frag_target.oldvelocity = frag_target.velocity; // prevents fall damage, see CreatureFrame_FallDamage - frag_target.avelocity = '0 0 0'; - frag_target.punchangle = '0 0 0'; - frag_target.punchvector = '0 0 0'; + if (frag_target.lives > 0 || (!frag_target.lives && STAT(FROZEN, frag_target) == FROZEN_NORMAL)) + { + // by restoring some health right after player death (soft-kill) + // weapons and ammo won't be reset + SetResourceExplicit(frag_target, RES_HEALTH, 1); + // restore armor as it was removed in PlayerDamage + SetResourceExplicit(frag_target, RES_ARMOR, frag_target.freezetag_frozen_armor); + + // relocate + entity spot = SelectSpawnPoint(frag_target, true); + setorigin(frag_target, spot.origin); + frag_target.oldorigin = frag_target.origin; + frag_target.fixangle = true; // turn this way immediately + frag_target.angles = vec2(spot.angles); + frag_target.velocity = '0 0 0'; + frag_target.oldvelocity = frag_target.velocity; // prevents fall damage, see CreatureFrame_FallDamage + frag_target.avelocity = '0 0 0'; + frag_target.punchangle = '0 0 0'; + frag_target.punchvector = '0 0 0'; + } } if (STAT(FROZEN, frag_target) == FROZEN_NORMAL) return true; - freezetag_Freeze(frag_target, frag_attacker); - freezetag_LastPlayerForTeam_Notify(frag_target); - - if(frag_attacker == frag_target || frag_attacker == NULL) + // player died and is not frozen + if (frag_target.lives > 0) { - if(IS_PLAYER(frag_target)) - Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_SELF); - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_SELF, frag_target.netname); + // freeze + freezetag_Freeze(frag_target, frag_attacker); + if (!warmup_stage) + { + if (frag_target.lives < FT_INFINITE_LIVES) + { + frag_target.lives--; + Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_LIVES_REMAINING, frag_target.lives); + } + } } else { - Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_FREEZE, frag_target.netname, frag_attacker.netname); + // schedule respawn, player will be moved to observer on respawn + frag_target.respawn_flags = RESPAWN_SILENT; + // prevent unwanted sudden rejoin as spectator and movement of spectator camera + frag_target.respawn_time = time + 2; + frag_target.respawn_flags |= RESPAWN_FORCE; + eliminatedPlayers.SendFlags |= 1; + frag_target.lives = -1; + freezetag_Add_Score(frag_target, frag_attacker); + freezetag_count_alive_players(); + } + freezetag_LastPlayerForTeam_Notify(frag_target); + + if (frag_target.lives > 0) + { + if(frag_attacker == frag_target || frag_attacker == NULL) + { + if(IS_PLAYER(frag_target)) + Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_FREEZETAG_SELF); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_SELF, frag_target.netname); + } + else + { + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_FREEZETAG_FREEZE, frag_target.netname, frag_attacker.netname); + } } return true; } +MUTATOR_HOOKFUNCTION(ft, ForbidSpawn) +{ + entity player = M_ARGV(0, entity); + + // if player lost all their lives and is observing, this check prevents any attempt + // to join that would only cause observer to respawn in another spawnpoint (as observer) + if (player.lives == -1) // player lost all their lives + return true; + + return false; +} + MUTATOR_HOOKFUNCTION(ft, PlayerSpawn) { entity player = M_ARGV(0, entity); @@ -425,6 +520,9 @@ MUTATOR_HOOKFUNCTION(ft, PlayerSpawn) return true; } + INGAME_STATUS_SET(player, INGAME_STATUS_JOINED); + player.lives = freezetag_Get_Start_Lives(); + freezetag_count_alive_players(); if(round_handler_IsActive()) @@ -439,15 +537,24 @@ MUTATOR_HOOKFUNCTION(ft, PlayerSpawn) MUTATOR_HOOKFUNCTION(ft, PutClientInServer) { + entity player = M_ARGV(0, entity); + + if (player.lives == -1 && IS_PLAYER(player)) // this is true even when player is trying to join + TRANSMUTE(Observer, player); + eliminatedPlayers.SendFlags |= 1; } MUTATOR_HOOKFUNCTION(ft, reset_map_players) { - FOREACH_CLIENT(IS_PLAYER(it), { + int start_lives = freezetag_Get_Start_Lives(); + FOREACH_CLIENT(INGAME(it) || IS_BOT_CLIENT(it), { CS(it).killcount = 0; it.freezetag_revive_time = 0; it.freezetag_frozen_timeout = -1; + INGAME_STATUS_SET(it, INGAME_STATUS_JOINED); + it.lives = start_lives; + TRANSMUTE(Player, it); PutClientInServer(it); it.freezetag_frozen_timeout = 0; }); @@ -685,6 +792,99 @@ MUTATOR_HOOKFUNCTION(ft, PlayerPreThink, CBC_ORDER_FIRST) return true; } +/** Returns next available player to spectate if g_freezetag_spectate_enemies == 0 */ +entity FT_SpectateNext(entity player, entity start) +{ + if (SAME_TEAM(start, player)) return start; + // continue from current player + for (entity e = start; (e = find(e, classname, STR_PLAYER)); ) + { + if (SAME_TEAM(player, e)) return e; + } + // restart from the beginning + for (entity e = NULL; (e = find(e, classname, STR_PLAYER)); ) + { + if (SAME_TEAM(player, e)) return e; + } + return start; +} + +MUTATOR_HOOKFUNCTION(ft, SpectateSet) +{ + entity client = M_ARGV(0, entity); + entity targ = M_ARGV(1, entity); + + if (g_freezetag_spectate_enemies != 1 && INGAME(client)) + if (DIFF_TEAM(targ, client)) + return true; +} + +MUTATOR_HOOKFUNCTION(ft, SpectateNext) +{ + entity client = M_ARGV(0, entity); + + if (g_freezetag_spectate_enemies != 1 && INGAME(client) + && Team_GetNumberOfAlivePlayers(Entity_GetTeam(client))) + { + entity targ = M_ARGV(1, entity); + M_ARGV(1, entity) = FT_SpectateNext(client, targ); + return true; + } +} + +MUTATOR_HOOKFUNCTION(ft, SpectatePrev) +{ + entity client = M_ARGV(0, entity); + entity targ = M_ARGV(1, entity); + entity first = M_ARGV(2, entity); + + if (g_freezetag_spectate_enemies != 1 && INGAME(client) + && Team_GetNumberOfAlivePlayers(Entity_GetTeam(client))) + { + do { targ = targ.chain; } + while(targ && DIFF_TEAM(targ, client)); + + if (!targ) + { + for (targ = first; targ && DIFF_TEAM(targ, client); targ = targ.chain); + + if (targ == client.enemy) + return MUT_SPECPREV_RETURN; + } + } + else + return MUT_SPECPREV_CONTINUE; + + M_ARGV(1, entity) = targ; + + return MUT_SPECPREV_FOUND; +} + +MUTATOR_HOOKFUNCTION(ft, Bot_FixCount, CBC_ORDER_EXCLUSIVE) +{ + FOREACH_CLIENT(IS_REAL_CLIENT(it), { + if (IS_PLAYER(it) || INGAME_JOINED(it)) + ++M_ARGV(0, int); + ++M_ARGV(1, int); + }); + return true; +} + +MUTATOR_HOOKFUNCTION(ft, ClientCommand_Spectate) +{ + entity player = M_ARGV(0, entity); + + if (INGAME(player)) + { + // they're going to spec, we can do other checks + if (autocvar_sv_spectate && (IS_SPEC(player) || IS_OBSERVER(player))) + Send_Notification(NOTIF_ONE_ONLY, player, MSG_INFO, INFO_CA_LEAVE); + return MUT_SPECCMD_FORCE; + } + + return MUT_SPECCMD_CONTINUE; +} + MUTATOR_HOOKFUNCTION(ft, SetStartItems) { start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS); @@ -776,6 +976,9 @@ void freezetag_Initialize() field(SP_FREEZETAG_REVIVALS, "revivals", 0); }); + g_freezetag_spectate_enemies = autocvar_g_freezetag_spectate_enemies; + observe_blocked_if_eliminated = (g_freezetag_spectate_enemies == -1); + round_handler_Spawn(freezetag_CheckTeams, freezetag_CheckWinner, func_null); round_handler_Init(5, autocvar_g_freezetag_warmup, autocvar_g_freezetag_round_timelimit); diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index dc118fac6..b29bb2ed0 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -695,6 +695,7 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MULTITEAM_CENTER(ROUND_TEAM_WIN, N_ENABLE, 0, 0, "", CPID_ROUND, "0 0", _("^TC^TT^BG team wins the round"), "", NAME) MSG_CENTER_NOTIF(ROUND_PLAYER_WIN, N_ENABLE, 1, 0, "s1", CPID_ROUND, "0 0", _("^BG%s^BG wins the round"), "") + MSG_CENTER_NOTIF(FREEZETAG_LIVES_REMAINING, N_ENABLE, 0, 1, "f1", CPID_Null, "0 0", _("^F2Lives remaining: ^K1%s"), "") MSG_CENTER_NOTIF(FREEZETAG_SELF, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^K1You froze yourself"), "") MSG_CENTER_NOTIF(FREEZETAG_SPAWN_LATE, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^K1Round already started, you spawn as frozen"), "") diff --git a/qcsrc/server/world.qc b/qcsrc/server/world.qc index 6e79c73ef..684dcae6c 100644 --- a/qcsrc/server/world.qc +++ b/qcsrc/server/world.qc @@ -372,6 +372,7 @@ void cvar_changes_init() BADCVAR("g_forced_respawn"); BADCVAR("g_freezetag_point_leadlimit"); BADCVAR("g_freezetag_point_limit"); + BADCVAR("g_freezetag_spectate_enemies"); BADCVAR("g_glowtrails"); BADCVAR("g_hats"); BADCVAR("g_casings"); -- 2.39.2