]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blobdiff - qcsrc/server/mutators/mutator/gamemode_keyhunt.qc
Don't even try to translate keys if game isn't translated
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_keyhunt.qc
index 944311598090c80e7f19f489ed2631ff52d1ff47..04576486b71bacd743304e7a5de42b709d3b95d7 100644 (file)
@@ -2,9 +2,11 @@
 
 float autocvar_g_balance_keyhunt_damageforcescale;
 float autocvar_g_balance_keyhunt_delay_collect;
+float autocvar_g_balance_keyhunt_delay_damage_return;
 float autocvar_g_balance_keyhunt_delay_return;
 float autocvar_g_balance_keyhunt_delay_round;
 float autocvar_g_balance_keyhunt_delay_tracking;
+float autocvar_g_balance_keyhunt_return_when_unreachable;
 float autocvar_g_balance_keyhunt_dropvelocity;
 float autocvar_g_balance_keyhunt_maxdist;
 float autocvar_g_balance_keyhunt_protecttime;
@@ -17,7 +19,7 @@ int autocvar_g_balance_keyhunt_score_destroyed_ownfactor;
 int autocvar_g_balance_keyhunt_score_push;
 float autocvar_g_balance_keyhunt_throwvelocity;
 
-int autocvar_g_keyhunt_teams;
+//int autocvar_g_keyhunt_teams;
 int autocvar_g_keyhunt_teams_override;
 
 // #define KH_PLAYER_USE_ATTACHMENT
@@ -47,17 +49,16 @@ bool kh_no_radar_circles;
 //     bits  5- 9: team of key 2, or 0 for no such key, or 30 for dropped, or 31 for self
 //     bits 10-14: team of key 3, or 0 for no such key, or 30 for dropped, or 31 for self
 //     bits 15-19: team of key 4, or 0 for no such key, or 30 for dropped, or 31 for self
-.float kh_state = _STAT(KH_KEYS);
 .float siren_time;  //  time delay the siren
 //.float stuff_time;  //  time delay to stuffcmd a cvar
 
-float kh_keystatus[17];
+int kh_keystatus[17];
 //kh_keystatus[0] = status of dropped keys, kh_keystatus[1 - 16] = player #
 //replace 17 with cvar("maxplayers") or similar !!!!!!!!!
 //for(i = 0; i < maxplayers; ++i)
 //     kh_keystatus[i] = "0";
 
-float kh_Team_ByID(float t)
+int kh_Team_ByID(int t)
 {
        if(t == 0) return NUM_TEAM_1;
        if(t == 1) return NUM_TEAM_2;
@@ -69,31 +70,32 @@ float kh_Team_ByID(float t)
 //entity kh_worldkeylist;
 .entity kh_worldkeynext;
 entity kh_controller;
-//float kh_tracking_enabled;
-float kh_teams;
-float kh_interferemsg_time, kh_interferemsg_team;
+//bool kh_tracking_enabled;
+int kh_teams;
+int kh_interferemsg_team;
+float kh_interferemsg_time;
 .entity kh_next, kh_prev; // linked list
 .float kh_droptime;
-.float kh_dropperteam;
+.int kh_dropperteam;
 .entity kh_previous_owner;
-.float kh_previous_owner_playerid;
+.int kh_previous_owner_playerid;
 
-float kh_key_dropped, kh_key_carried;
+int kh_key_dropped, kh_key_carried;
 
 int kh_Key_AllOwnedByWhichTeam();
 
-const float ST_KH_CAPS = 1;
+const int ST_KH_CAPS = 1;
 void kh_ScoreRules(int teams)
 {
-       ScoreRules_basics(teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, true);
-       ScoreInfo_SetLabel_TeamScore(  ST_KH_CAPS,      "caps",      SFL_SORT_PRIO_SECONDARY);
-       ScoreInfo_SetLabel_PlayerScore(SP_KH_CAPS,      "caps",      SFL_SORT_PRIO_SECONDARY);
-       ScoreInfo_SetLabel_PlayerScore(SP_KH_PUSHES,    "pushes",    0);
-       ScoreInfo_SetLabel_PlayerScore(SP_KH_DESTROYS,  "destroyed", SFL_LOWER_IS_BETTER);
-       ScoreInfo_SetLabel_PlayerScore(SP_KH_PICKUPS,   "pickups",   0);
-       ScoreInfo_SetLabel_PlayerScore(SP_KH_KCKILLS,   "kckills",   0);
-       ScoreInfo_SetLabel_PlayerScore(SP_KH_LOSSES,    "losses",    SFL_LOWER_IS_BETTER);
-       ScoreRules_basics_end();
+       GameRules_scoring(teams, SFL_SORT_PRIO_PRIMARY, SFL_SORT_PRIO_PRIMARY, {
+        field_team(ST_KH_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
+        field(SP_KH_CAPS, "caps", SFL_SORT_PRIO_SECONDARY);
+        field(SP_KH_PUSHES, "pushes", 0);
+        field(SP_KH_DESTROYS, "destroyed", SFL_LOWER_IS_BETTER);
+        field(SP_KH_PICKUPS, "pickups", 0);
+        field(SP_KH_KCKILLS, "kckills", 0);
+        field(SP_KH_LOSSES, "losses", SFL_LOWER_IS_BETTER);
+       });
 }
 
 bool kh_KeyCarrier_waypointsprite_visible_for_player(entity this, entity player, entity view)  // runs all the time
@@ -119,25 +121,23 @@ bool kh_Key_waypointsprite_visible_for_player(entity this, entity player, entity
 void kh_update_state()
 {
        entity key;
-       float s;
-       float f;
-
-       s = 0;
+       int f;
+       int s = 0;
        FOR_EACH_KH_KEY(key)
        {
                if(key.owner)
                        f = key.team;
                else
                        f = 30;
-               s |= pow(32, key.count) * f;
+               s |= (32 ** key.count) * f;
        }
 
-       FOREACH_CLIENT(true, LAMBDA(it.kh_state = s));
+       FOREACH_CLIENT(true, { STAT(KH_KEYS, it) = s; });
 
        FOR_EACH_KH_KEY(key)
        {
                if(key.owner)
-                       key.owner.kh_state |= pow(32, key.count) * 31;
+                       STAT(KH_KEYS, key.owner) |= (32 ** key.count) * 31;
        }
        //print(ftos((nextent(NULL)).kh_state), "\n");
 }
@@ -156,10 +156,13 @@ void kh_Controller_SetThink(float t, kh_Think_t func)  // runs occasionaly
 void kh_WaitForPlayers();
 void kh_Controller_Think(entity this)  // called a lot
 {
-       if(intermission_running)
+       if(game_stopped)
                return;
        if(this.cnt > 0)
-       { if(getthink(this) != kh_WaitForPlayers) { this.cnt -= 1; } }
+       {
+               if(getthink(this) != kh_WaitForPlayers)
+                       this.cnt -= 1;
+       }
        else if(this.cnt == 0)
        {
                this.cnt -= 1;
@@ -173,7 +176,7 @@ void kh_Controller_Think(entity this)  // called a lot
 void kh_Scores_Event(entity player, entity key, string what, float frags_player, float frags_owner)  // update the score when a key is captured
 {
        string s;
-       if(intermission_running)
+       if(game_stopped)
                return;
 
        if(frags_player)
@@ -214,8 +217,7 @@ vector kh_AttachedOrigin(entity e)  // runs when a team captures the flag, it ca
 void kh_Key_Attach(entity key)  // runs when a player picks up a key and several times when a key is assigned to a player at the start of a round
 {
 #ifdef KH_PLAYER_USE_ATTACHMENT
-       entity first;
-       first = key.owner.kh_next;
+       entity first = key.owner.kh_next;
        if(key == first)
        {
                setattachment(key, key.owner, KH_PLAYER_ATTACHMENT_BONE);
@@ -245,7 +247,8 @@ void kh_Key_Attach(entity key)  // runs when a player picks up a key and several
        key.angles_y -= key.owner.angles.y;
 #endif
        key.flags = 0;
-       IL_REMOVE(g_items, key);
+       if(IL_CONTAINS(g_items, key))
+               IL_REMOVE(g_items, key);
        key.solid = SOLID_NOT;
        set_movetype(key, MOVETYPE_NONE);
        key.team = key.owner.team;
@@ -253,13 +256,13 @@ void kh_Key_Attach(entity key)  // runs when a player picks up a key and several
        key.damageforcescale = 0;
        key.takedamage = DAMAGE_NO;
        key.modelindex = kh_key_carried;
+       navigation_dynamicgoal_unset(key);
 }
 
 void kh_Key_Detach(entity key) // runs every time a key is dropped or lost. Runs several times times when all the keys are captured
 {
 #ifdef KH_PLAYER_USE_ATTACHMENT
-       entity first;
-       first = key.owner.kh_next;
+       entity first = key.owner.kh_next;
        if(key == first)
        {
                if(key.kh_next)
@@ -277,7 +280,7 @@ void kh_Key_Detach(entity key) // runs every time a key is dropped or lost. Runs
        }
        // in any case:
        setattachment(key, NULL, "");
-       setorigin(key, key.owner.origin + '0 0 1' * (STAT(PL_MIN, NULL).z - KH_KEY_MIN_z));
+       setorigin(key, key.owner.origin + '0 0 1' * (STAT(PL_MIN, key.owner).z - KH_KEY_MIN_z));
        key.angles = key.owner.angles;
 #else
        setorigin(key, key.owner.origin + key.origin.z * '0 0 1');
@@ -285,7 +288,8 @@ void kh_Key_Detach(entity key) // runs every time a key is dropped or lost. Runs
        key.angles_y += key.owner.angles.y;
 #endif
        key.flags = FL_ITEM;
-       IL_PUSH(g_items, key);
+       if(!IL_CONTAINS(g_items, key))
+               IL_PUSH(g_items, key);
        key.solid = SOLID_TRIGGER;
        set_movetype(key, MOVETYPE_TOSS);
        key.pain_finished = time + autocvar_g_balance_keyhunt_delay_return;
@@ -293,18 +297,17 @@ void kh_Key_Detach(entity key) // runs every time a key is dropped or lost. Runs
        key.takedamage = DAMAGE_YES;
        // let key.team stay
        key.modelindex = kh_key_dropped;
+       navigation_dynamicgoal_set(key);
        key.kh_previous_owner = key.owner;
        key.kh_previous_owner_playerid = key.owner.playerid;
 }
 
 void kh_Key_AssignTo(entity key, entity player)  // runs every time a key is picked up or assigned. Runs prior to kh_key_attach
 {
-       entity k;
-       float ownerteam0, ownerteam;
        if(key.owner == player)
                return;
 
-       ownerteam0 = kh_Key_AllOwnedByWhichTeam();
+       int ownerteam0 = kh_Key_AllOwnedByWhichTeam();
 
        if(key.owner)
        {
@@ -376,22 +379,23 @@ void kh_Key_AssignTo(entity key, entity player)  // runs every time a key is pic
 
        key.pusher = NULL;
 
-       ownerteam = kh_Key_AllOwnedByWhichTeam();
+       int ownerteam = kh_Key_AllOwnedByWhichTeam();
        if(ownerteam != ownerteam0)
        {
+               entity k;
                if(ownerteam != -1)
                {
                        kh_interferemsg_time = time + 0.2;
                        kh_interferemsg_team = player.team;
 
-                       // audit all key carrier sprites, update them to RUN HERE
+                       // audit all key carrier sprites, update them to "Run here"
                        FOR_EACH_KH_KEY(k)
                        {
                                if (!k.owner) continue;
                                entity first = WP_Null;
-                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, LAMBDA(first = it; break));
+                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, { first = it; break; });
                                entity third = WP_Null;
-                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, LAMBDA(third = it; break));
+                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, { third = it; break; });
                                WaypointSprite_UpdateSprites(k.owner.waypointsprite_attachedforcarrier, first, WP_KeyCarrierFinish, third);
                        }
                }
@@ -399,30 +403,28 @@ void kh_Key_AssignTo(entity key, entity player)  // runs every time a key is pic
                {
                        kh_interferemsg_time = 0;
 
-                       // audit all key carrier sprites, update them to RUN HERE
+                       // audit all key carrier sprites, update them to "Key Carrier"
                        FOR_EACH_KH_KEY(k)
                        {
                                if (!k.owner) continue;
                                entity first = WP_Null;
-                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, LAMBDA(first = it; break));
+                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model1, { first = it; break; });
                                entity third = WP_Null;
-                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, LAMBDA(third = it; break));
+                               FOREACH(Waypoints, it.netname == k.owner.waypointsprite_attachedforcarrier.model3, { third = it; break; });
                                WaypointSprite_UpdateSprites(k.owner.waypointsprite_attachedforcarrier, first, WP_KeyCarrierFriend, third);
                        }
                }
        }
 }
 
-void kh_Key_Damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
+void kh_Key_Damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
 {
        if(this.owner)
                return;
        if(ITEM_DAMAGE_NEEDKILL(deathtype))
        {
-               // touching lava, or hurt trigger
-               // what shall we do?
-               // immediately return is bad
-               // maybe start a shorter countdown?
+               this.pain_finished = bound(time, time + autocvar_g_balance_keyhunt_delay_damage_return, this.pain_finished);
+               return;
        }
        if(force == '0 0 0')
                return;
@@ -438,17 +440,18 @@ void kh_Key_Collect(entity key, entity player)  //a player picks up a dropped ke
        if(key.kh_dropperteam != player.team)
        {
                kh_Scores_Event(player, key, "collect", autocvar_g_balance_keyhunt_score_collect, 0);
-               PlayerScore_Add(player, SP_KH_PICKUPS, 1);
+               GameRules_scoring_add(player, KH_PICKUPS, 1);
        }
        key.kh_dropperteam = 0;
-       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(key.team, INFO_KEYHUNT_PICKUP), player.netname);
+       int realteam = kh_Team_ByID(key.count);
+       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(realteam, INFO_KEYHUNT_PICKUP), player.netname);
 
        kh_Key_AssignTo(key, player); // this also updates .kh_state
 }
 
 void kh_Key_Touch(entity this, entity toucher)  // runs many, many times when a key has been dropped and can be picked up
 {
-       if(intermission_running)
+       if(game_stopped)
                return;
 
        if(this.owner) // already carried
@@ -456,10 +459,8 @@ void kh_Key_Touch(entity this, entity toucher)  // runs many, many times when a
 
        if(ITEM_TOUCH_NEEDKILL())
        {
-               // touching sky, or nodrop
-               // what shall we do?
-               // immediately return is bad
-               // maybe start a shorter countdown?
+               this.pain_finished = bound(time, time + autocvar_g_balance_keyhunt_delay_damage_return, this.pain_finished);
+               return;
        }
 
        if (!IS_PLAYER(toucher))
@@ -474,8 +475,7 @@ void kh_Key_Touch(entity this, entity toucher)  // runs many, many times when a
 
 void kh_Key_Remove(entity key)  // runs after when all the keys have been collected or when a key has been dropped for more than X seconds
 {
-       entity o;
-       o = key.owner;
+       entity o = key.owner;
        kh_Key_AssignTo(key, NULL);
        if(o) // it was attached
                WaypointSprite_Kill(key.waypointsprite_attachedforcarrier);
@@ -521,27 +521,23 @@ void kh_FinishRound()  // runs when a team captures the keys
 
 void nades_GiveBonus(entity player, float score);
 
-void kh_WinnerTeam(float teem)  // runs when a team wins // Samual: Teem?.... TEEM?!?! what the fuck is wrong with you people
+void kh_WinnerTeam(int winner_team)  // runs when a team wins
 {
        // all key carriers get some points
-       vector firstorigin, lastorigin, midpoint;
-       float first;
        entity key;
-       float score;
-       score = (NumTeams(kh_teams) - 1) * autocvar_g_balance_keyhunt_score_capture;
+       float score = (NumTeams(kh_teams) - 1) * autocvar_g_balance_keyhunt_score_capture;
        DistributeEvenly_Init(score, NumTeams(kh_teams));
        // twice the score for 3 team games, three times the score for 4 team games!
        // note: for a win by destroying the key, this should NOT be applied
        FOR_EACH_KH_KEY(key)
        {
-               float f;
-               f = DistributeEvenly_Get(1);
+               float f = DistributeEvenly_Get(1);
                kh_Scores_Event(key.owner, key, "capture", f, 0);
-               PlayerTeamScore_Add(key.owner, SP_KH_CAPS, ST_KH_CAPS, 1);
+               GameRules_scoring_add_team(key.owner, KH_CAPS, 1);
                nades_GiveBonus(key.owner, autocvar_g_nades_bonus_score_high);
        }
 
-       first = true;
+       bool first = true;
        string keyowner = "";
        FOR_EACH_KH_KEY(key)
                if(key.owner.kh_next == key)
@@ -552,17 +548,14 @@ void kh_WinnerTeam(float teem)  // runs when a team wins // Samual: Teem?.... TE
                        first = false;
                }
 
-       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(teem, INFO_KEYHUNT_CAPTURE), keyowner);
+       Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(winner_team, CENTER_ROUND_TEAM_WIN));
+       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(winner_team, INFO_KEYHUNT_CAPTURE), keyowner);
 
        first = true;
-       midpoint = '0 0 0';
-       firstorigin = '0 0 0';
-       lastorigin = '0 0 0';
+       vector firstorigin = '0 0 0', lastorigin = '0 0 0', midpoint = '0 0 0';
        FOR_EACH_KH_KEY(key)
        {
-               vector thisorigin;
-
-               thisorigin = kh_AttachedOrigin(key);
+               vector thisorigin = kh_AttachedOrigin(key);
                //dprint("Key origin: ", vtos(thisorigin), "\n");
                midpoint += thisorigin;
 
@@ -578,45 +571,41 @@ void kh_WinnerTeam(float teem)  // runs when a team wins // Samual: Teem?.... TE
                te_lightning2(NULL, lastorigin, firstorigin);
        }
        midpoint = midpoint * (1 / NumTeams(kh_teams));
-       te_customflash(midpoint, 1000, 1, Team_ColorRGB(teem) * 0.5 + '0.5 0.5 0.5');  // make the color >=0.5 in each component
+       te_customflash(midpoint, 1000, 1, Team_ColorRGB(winner_team) * 0.5 + '0.5 0.5 0.5');  // make the color >=0.5 in each component
 
        play2all(SND(KH_CAPTURE));
        kh_FinishRound();
 }
 
-void kh_LoserTeam(float teem, entity lostkey)  // runs when a player pushes a flag carrier off the map
+void kh_LoserTeam(int loser_team, entity lostkey)  // runs when a player pushes a flag carrier off the map
 {
-       entity key, attacker;
-       float players;
-       float keys;
        float f;
-
-       attacker = NULL;
+       entity attacker = NULL;
        if(lostkey.pusher)
-               if(lostkey.pusher.team != teem)
+               if(lostkey.pusher.team != loser_team)
                        if(IS_PLAYER(lostkey.pusher))
                                attacker = lostkey.pusher;
 
-       players = keys = 0;
-
        if(attacker)
        {
                if(lostkey.kh_previous_owner)
                        kh_Scores_Event(lostkey.kh_previous_owner, NULL, "pushed", 0, -autocvar_g_balance_keyhunt_score_push);
                        // don't actually GIVE him the -nn points, just log
                kh_Scores_Event(attacker, NULL, "push", autocvar_g_balance_keyhunt_score_push, 0);
-               PlayerScore_Add(attacker, SP_KH_PUSHES, 1);
+               GameRules_scoring_add(attacker, KH_PUSHES, 1);
                //centerprint(attacker, "Your push is the best!"); // does this really need to exist?
        }
        else
        {
-               float of, fragsleft, i, j, thisteam;
-               of = autocvar_g_balance_keyhunt_score_destroyed_ownfactor;
+               int players = 0;
+               float of = autocvar_g_balance_keyhunt_score_destroyed_ownfactor;
 
-               FOREACH_CLIENT(IS_PLAYER(it) && it.team != teem, LAMBDA(++players));
+               FOREACH_CLIENT(IS_PLAYER(it) && it.team != loser_team, { ++players; });
 
+               entity key;
+               int keys = 0;
                FOR_EACH_KH_KEY(key)
-                       if(key.owner && key.team != teem)
+                       if(key.owner && key.team != loser_team)
                                ++keys;
 
                if(lostkey.kh_previous_owner)
@@ -624,44 +613,49 @@ void kh_LoserTeam(float teem, entity lostkey)  // runs when a player pushes a fl
                        // don't actually GIVE him the -nn points, just log
 
                if(lostkey.kh_previous_owner.playerid == lostkey.kh_previous_owner_playerid)
-                       PlayerScore_Add(lostkey.kh_previous_owner, SP_KH_DESTROYS, 1);
+                       GameRules_scoring_add(lostkey.kh_previous_owner, KH_DESTROYS, 1);
 
                DistributeEvenly_Init(autocvar_g_balance_keyhunt_score_destroyed, keys * of + players);
 
                FOR_EACH_KH_KEY(key)
-                       if(key.owner && key.team != teem)
+                       if(key.owner && key.team != loser_team)
                        {
                                f = DistributeEvenly_Get(of);
                                kh_Scores_Event(key.owner, NULL, "destroyed_holdingkey", f, 0);
                        }
 
-               fragsleft = DistributeEvenly_Get(players);
+               int fragsleft = DistributeEvenly_Get(players);
 
                // Now distribute these among all other teams...
-               j = NumTeams(kh_teams) - 1;
-               for(i = 0; i < NumTeams(kh_teams); ++i)
+               int j = NumTeams(kh_teams) - 1;
+               for(int i = 0; i < NumTeams(kh_teams); ++i)
                {
-                       thisteam = kh_Team_ByID(i);
-                       if(thisteam == teem) // bad boy, no cookie - this WILL happen
+                       int thisteam = kh_Team_ByID(i);
+                       if(thisteam == loser_team) // bad boy, no cookie - this WILL happen
                                continue;
 
                        players = 0;
-                       FOREACH_CLIENT(IS_PLAYER(it) && it.team == thisteam, LAMBDA(++players));
+                       FOREACH_CLIENT(IS_PLAYER(it) && it.team == thisteam, { ++players; });
 
                        DistributeEvenly_Init(fragsleft, j);
                        fragsleft = DistributeEvenly_Get(j - 1);
                        DistributeEvenly_Init(DistributeEvenly_Get(1), players);
 
-                       FOREACH_CLIENT(IS_PLAYER(it) && it.team == thisteam, LAMBDA(
+                       FOREACH_CLIENT(IS_PLAYER(it) && it.team == thisteam, {
                                f = DistributeEvenly_Get(1);
                                kh_Scores_Event(it, NULL, "destroyed", f, 0);
-                       ));
+                       });
 
                        --j;
                }
        }
 
-       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(lostkey.team, INFO_KEYHUNT_LOST), lostkey.kh_previous_owner.netname);
+       int realteam = kh_Team_ByID(lostkey.count);
+       Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, APP_TEAM_NUM(loser_team, CENTER_ROUND_TEAM_LOSS));
+       if(attacker)
+               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(realteam, INFO_KEYHUNT_PUSHED), attacker.netname, lostkey.kh_previous_owner.netname);
+       else
+               Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(realteam, INFO_KEYHUNT_DESTROYED), lostkey.kh_previous_owner.netname);
 
        play2all(SND(KH_DESTROY));
        te_tarexplosion(lostkey.origin);
@@ -671,7 +665,7 @@ void kh_LoserTeam(float teem, entity lostkey)  // runs when a player pushes a fl
 
 void kh_Key_Think(entity this)  // runs all the time
 {
-       if(intermission_running)
+       if(game_stopped)
                return;
 
        if(this.owner)
@@ -697,8 +691,7 @@ void kh_Key_Think(entity this)  // runs all the time
                }
 
                entity key;
-               vector p;
-               p = this.owner.origin;
+               vector p = this.owner.origin;
                FOR_EACH_KH_KEY(key)
                        if(vdist(key.owner.origin - p, >, autocvar_g_balance_keyhunt_maxdist))
                                goto not_winning;
@@ -709,7 +702,7 @@ LABEL(not_winning)
        if(kh_interferemsg_time && time > kh_interferemsg_time)
        {
                kh_interferemsg_time = 0;
-               FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
+               FOREACH_CLIENT(IS_PLAYER(it), {
                        if(it.team == kh_interferemsg_team)
                                if(it.kh_next)
                                        Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_KEYHUNT_MEET);
@@ -717,7 +710,7 @@ LABEL(not_winning)
                                        Send_Notification(NOTIF_ONE, it, MSG_CENTER, CENTER_KEYHUNT_HELP);
                        else
                                Send_Notification(NOTIF_ONE, it, MSG_CENTER, APP_TEAM_NUM(kh_interferemsg_team, CENTER_KEYHUNT_INTERFERE));
-               ));
+               });
        }
 
        this.nextthink = time + 0.05;
@@ -743,6 +736,8 @@ void kh_Key_Spawn(entity initial_owner, float _angle, float i)  // runs every ti
        key.angles = '0 360 0' * random();
        key.event_damage = kh_Key_Damage;
        key.takedamage = DAMAGE_YES;
+       key.damagedbytriggers = autocvar_g_balance_keyhunt_return_when_unreachable;
+       key.damagedbycontents = autocvar_g_balance_keyhunt_return_when_unreachable;
        key.modelindex = kh_key_dropped;
        key.model = "key";
        key.kh_dropperteam = 0;
@@ -750,6 +745,7 @@ void kh_Key_Spawn(entity initial_owner, float _angle, float i)  // runs every ti
        setsize(key, KH_KEY_MIN, KH_KEY_MAX);
        key.colormod = Team_ColorRGB(initial_owner.team) * KH_KEY_BRIGHTNESS;
        key.reset = key_reset;
+       navigation_dynamicgoal_init(key, false);
 
        switch(initial_owner.team)
        {
@@ -806,15 +802,15 @@ int kh_Key_AllOwnedByWhichTeam()  // constantly called. check to see if all the
 void kh_Key_DropOne(entity key)
 {
        // prevent collecting this one for some time
-       entity player;
-       player = key.owner;
+       entity player = key.owner;
 
        key.kh_droptime = time;
        key.enemy = player;
 
        kh_Scores_Event(player, key, "dropkey", 0, 0);
-       PlayerScore_Add(player, SP_KH_LOSSES, 1);
-       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(key.team, INFO_KEYHUNT_DROP), player.netname);
+       GameRules_scoring_add(player, KH_LOSSES, 1);
+       int realteam = kh_Team_ByID(key.count);
+       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(realteam, INFO_KEYHUNT_DROP), player.netname);
 
        kh_Key_AssignTo(key, NULL);
        makevectors(player.v_angle);
@@ -828,19 +824,20 @@ void kh_Key_DropOne(entity key)
 
 void kh_Key_DropAll(entity player, float suicide) // runs whenever a player dies
 {
-       entity key;
-       entity mypusher;
        if(player.kh_next)
        {
-               mypusher = NULL;
+               entity mypusher = NULL;
                if(player.pusher)
                        if(time < player.pushltime)
                                mypusher = player.pusher;
+
+               entity key;
                while((key = player.kh_next))
                {
                        kh_Scores_Event(player, key, "losekey", 0, 0);
-                       PlayerScore_Add(player, SP_KH_LOSSES, 1);
-                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(key.team, INFO_KEYHUNT_LOST), player.netname);
+                       GameRules_scoring_add(player, KH_LOSSES, 1);
+                       int realteam = kh_Team_ByID(key.count);
+                       Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(realteam, INFO_KEYHUNT_LOST), player.netname);
                        kh_Key_AssignTo(key, NULL);
                        makevectors('-1 0 0' * (45 + 45 * random()) + '0 360 0' * random());
                        key.velocity = W_CalculateProjectileVelocity(player, player.velocity, autocvar_g_balance_keyhunt_dropvelocity * v_forward, false);
@@ -860,25 +857,28 @@ int kh_GetMissingTeams()
        {
                int teem = kh_Team_ByID(i);
                int players = 0;
-               FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
+               FOREACH_CLIENT(IS_PLAYER(it), {
                        if(!IS_DEAD(it) && !PHYS_INPUT_BUTTON_CHAT(it) && it.team == teem)
                                ++players;
-               ));
+               });
                if (!players)
-                       missing_teams |= pow(2, i);
+                       missing_teams |= (2 ** i);
        }
        return missing_teams;
 }
 
 void kh_WaitForPlayers()  // delay start of the round until enough players are present
 {
+       static int prev_missing_teams_mask;
        if(time < game_starttime)
        {
+               if (prev_missing_teams_mask > 0)
+                       Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS);
+               prev_missing_teams_mask = -1;
                kh_Controller_SetThink(game_starttime - time + 0.1, kh_WaitForPlayers);
                return;
        }
 
-       static int prev_missing_teams_mask;
        int missing_teams_mask = kh_GetMissingTeams();
        if(!missing_teams_mask)
        {
@@ -918,8 +918,6 @@ void kh_EnableTrackingDevice()  // runs after each round
 
 void kh_StartRound()  // runs at the start of each round
 {
-       int i, players, teem;
-
        if(time < game_starttime)
        {
                kh_Controller_SetThink(game_starttime - time + 0.1, kh_WaitForPlayers);
@@ -935,19 +933,19 @@ void kh_StartRound()  // runs at the start of each round
        Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_KEYHUNT);
        Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_KEYHUNT_OTHER);
 
-       for(i = 0; i < NumTeams(kh_teams); ++i)
+       for(int i = 0; i < NumTeams(kh_teams); ++i)
        {
-               teem = kh_Team_ByID(i);
-               players = 0;
+               int teem = kh_Team_ByID(i);
+               int players = 0;
                entity my_player = NULL;
-               FOREACH_CLIENT(IS_PLAYER(it), LAMBDA(
+               FOREACH_CLIENT(IS_PLAYER(it), {
                        if(!IS_DEAD(it) && !PHYS_INPUT_BUTTON_CHAT(it) && it.team == teem)
                        {
                                ++players;
                                if(random() * players <= 1)
                                        my_player = it;
                        }
-               ));
+               });
                kh_Key_Spawn(my_player, 360 * i / NumTeams(kh_teams), i);
        }
 
@@ -965,17 +963,15 @@ float kh_HandleFrags(entity attacker, entity targ, float f)  // adds to the play
        {
                if(attacker.team == targ.team)
                {
-                       entity k;
-                       float nk;
-                       nk = 0;
-                       for(k = targ.kh_next; k != NULL; k = k.kh_next)
+                       int nk = 0;
+                       for(entity k = targ.kh_next; k != NULL; k = k.kh_next)
                                ++nk;
                        kh_Scores_Event(attacker, targ.kh_next, "carrierfrag", -nk * autocvar_g_balance_keyhunt_score_collect, 0);
                }
                else
                {
                        kh_Scores_Event(attacker, targ.kh_next, "carrierfrag", autocvar_g_balance_keyhunt_score_carrierfrag-1, 0);
-                       PlayerScore_Add(attacker, SP_KH_KCKILLS, 1);
+                       GameRules_scoring_add(attacker, KH_KCKILLS, 1);
                        // the frag gets added later
                }
        }
@@ -988,16 +984,8 @@ void kh_Initialize()  // sets up th KH environment
        // setup variables
        kh_teams = autocvar_g_keyhunt_teams_override;
        if(kh_teams < 2)
-               kh_teams = autocvar_g_keyhunt_teams;
-       kh_teams = bound(2, kh_teams, 4);
-
-       int teams = 0;
-       if(kh_teams >= 1) teams |= BIT(0);
-       if(kh_teams >= 2) teams |= BIT(1);
-       if(kh_teams >= 3) teams |= BIT(2);
-       if(kh_teams >= 4) teams |= BIT(3);
-
-       kh_teams = teams; // now set it?
+               kh_teams = cvar("g_keyhunt_teams"); // read the cvar directly as it gets written earlier in the same frame
+       kh_teams = BITS(bound(2, kh_teams, 4));
 
        // make a KH entity for controlling the game
        kh_controller = spawn();
@@ -1059,11 +1047,11 @@ void havocbot_goalrating_kh(entity this, float ratingscale_team, float ratingsca
                        }
                }
                if(!head.owner)
-                       navigation_routerating(this, head, ratingscale_dropped * BOT_PICKUP_RATING_HIGH, 100000);
+                       navigation_routerating(this, head, ratingscale_dropped * 10000, 100000);
                else if(head.team == this.team)
-                       navigation_routerating(this, head.owner, ratingscale_team * BOT_PICKUP_RATING_HIGH, 100000);
+                       navigation_routerating(this, head.owner, ratingscale_team * 10000, 100000);
                else
-                       navigation_routerating(this, head.owner, ratingscale_enemy * BOT_PICKUP_RATING_HIGH, 100000);
+                       navigation_routerating(this, head.owner, ratingscale_enemy * 10000, 100000);
        }
 
        havocbot_goalrating_items(this, 1, this.origin, 10000);
@@ -1082,9 +1070,8 @@ void havocbot_role_kh_carrier(entity this)
                return;
        }
 
-       if (this.bot_strategytime < time)
+       if (navigation_goalrating_timeout(this))
        {
-               this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
 
                if(kh_Key_AllOwnedByWhichTeam() == this.team)
@@ -1093,6 +1080,8 @@ void havocbot_role_kh_carrier(entity this)
                        havocbot_goalrating_kh(this, 4, 4, 1); // play defensively
 
                navigation_goalrating_end(this);
+
+               navigation_goalrating_timeout_set(this);
        }
 }
 
@@ -1119,10 +1108,9 @@ void havocbot_role_kh_defense(entity this)
                return;
        }
 
-       if (this.bot_strategytime < time)
+       if (navigation_goalrating_timeout(this))
        {
                float key_owner_team;
-               this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
 
                key_owner_team = kh_Key_AllOwnedByWhichTeam();
@@ -1134,6 +1122,8 @@ void havocbot_role_kh_defense(entity this)
                        havocbot_goalrating_kh(this, 0.1, 0.1, 10); // ATTACK ANYWAY
 
                navigation_goalrating_end(this);
+
+               navigation_goalrating_timeout_set(this);
        }
 }
 
@@ -1160,11 +1150,10 @@ void havocbot_role_kh_offense(entity this)
                return;
        }
 
-       if (this.bot_strategytime < time)
+       if (navigation_goalrating_timeout(this))
        {
                float key_owner_team;
 
-               this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
 
                key_owner_team = kh_Key_AllOwnedByWhichTeam();
@@ -1176,6 +1165,8 @@ void havocbot_role_kh_offense(entity this)
                        havocbot_goalrating_kh(this, 0.1, 0.1, 10); // ATTACK! EMERGENCY!
 
                navigation_goalrating_end(this);
+
+               navigation_goalrating_timeout_set(this);
        }
 }
 
@@ -1210,14 +1201,11 @@ void havocbot_role_kh_freelancer(entity this)
                return;
        }
 
-       if (this.bot_strategytime < time)
+       if (navigation_goalrating_timeout(this))
        {
-               float key_owner_team;
-
-               this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
                navigation_goalrating_start(this);
 
-               key_owner_team = kh_Key_AllOwnedByWhichTeam();
+               int key_owner_team = kh_Key_AllOwnedByWhichTeam();
                if(key_owner_team == this.team)
                        havocbot_goalrating_kh(this, 10, 0.1, 0.1); // defend anyway
                else if(key_owner_team == -1)
@@ -1226,6 +1214,8 @@ void havocbot_role_kh_freelancer(entity this)
                        havocbot_goalrating_kh(this, 0.1, 0.1, 10); // ATTACK ANYWAY
 
                navigation_goalrating_end(this);
+
+               navigation_goalrating_timeout_set(this);
        }
 }
 
@@ -1282,7 +1272,7 @@ MUTATOR_HOOKFUNCTION(kh, SpectateCopy)
        entity spectatee = M_ARGV(0, entity);
        entity client = M_ARGV(1, entity);
 
-       client.kh_state = spectatee.kh_state;
+       STAT(KH_KEYS, client) = STAT(KH_KEYS, spectatee);
 }
 
 MUTATOR_HOOKFUNCTION(kh, PlayerUseKey)
@@ -1327,5 +1317,5 @@ MUTATOR_HOOKFUNCTION(kh, DropSpecialItems)
 
 MUTATOR_HOOKFUNCTION(kh, reset_map_global)
 {
-       kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_round + (game_starttime - time), kh_StartRound);
+       kh_WaitForPlayers(); // takes care of killing the "missing teams" message
 }