X-Git-Url: http://git.xonotic.org/?a=blobdiff_plain;f=qcsrc%2Fcommon%2Fgamemodes%2Fgamemode%2Fkeyhunt%2Fsv_keyhunt.qc;fp=qcsrc%2Fcommon%2Fgamemodes%2Fgamemode%2Fkeyhunt%2Fsv_keyhunt.qc;h=cce2ffb747076bf03485eb9fbee059037de3a3dc;hb=9989aca77459e9bd305c86d71b1b6912ebca9a4e;hp=0000000000000000000000000000000000000000;hpb=f6dc194f6394a97881b890a7f07eadd60e00591e;p=xonotic%2Fxonotic-data.pk3dir.git diff --git a/qcsrc/common/gamemodes/gamemode/keyhunt/sv_keyhunt.qc b/qcsrc/common/gamemodes/gamemode/keyhunt/sv_keyhunt.qc new file mode 100644 index 000000000..cce2ffb74 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/keyhunt/sv_keyhunt.qc @@ -0,0 +1,1322 @@ +#include "sv_keyhunt.qh" + +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; + +int autocvar_g_balance_keyhunt_score_capture; +int autocvar_g_balance_keyhunt_score_carrierfrag; +int autocvar_g_balance_keyhunt_score_collect; +int autocvar_g_balance_keyhunt_score_destroyed; +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_override; + +// #define KH_PLAYER_USE_ATTACHMENT +// #define KH_PLAYER_USE_CARRIEDMODEL + +#ifdef KH_PLAYER_USE_ATTACHMENT +const vector KH_PLAYER_ATTACHMENT_DIST_ROTATED = '0 -4 0'; +const vector KH_PLAYER_ATTACHMENT_DIST = '4 0 0'; +const vector KH_PLAYER_ATTACHMENT = '0 0 0'; +const vector KH_PLAYER_ATTACHMENT_ANGLES = '0 0 0'; +const string KH_PLAYER_ATTACHMENT_BONE = ""; +#else +const float KH_KEY_ZSHIFT = 22; +const float KH_KEY_XYDIST = 24; +const float KH_KEY_XYSPEED = 45; +#endif +const float KH_KEY_WP_ZSHIFT = 20; + +const vector KH_KEY_MIN = '-10 -10 -46'; +const vector KH_KEY_MAX = '10 10 3'; +const float KH_KEY_BRIGHTNESS = 2; + +bool kh_no_radar_circles; + +// kh_state +// bits 0- 4: team of key 1, or 0 for no such key, or 30 for dropped, or 31 for self +// 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 siren_time; // time delay the siren +//.float stuff_time; // time delay to stuffcmd a cvar + +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"; + +int kh_Team_ByID(int t) +{ + if(t == 0) return NUM_TEAM_1; + if(t == 1) return NUM_TEAM_2; + if(t == 2) return NUM_TEAM_3; + if(t == 3) return NUM_TEAM_4; + return 0; +} + +//entity kh_worldkeylist; +.entity kh_worldkeynext; +entity kh_controller; +//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; +.int kh_dropperteam; +.entity kh_previous_owner; +.int kh_previous_owner_playerid; + +int kh_key_dropped, kh_key_carried; + +int kh_Key_AllOwnedByWhichTeam(); + +const int ST_KH_CAPS = 1; +void kh_ScoreRules(int teams) +{ + 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 +{ + if(!IS_PLAYER(view) || DIFF_TEAM(this, view)) + if(!kh_tracking_enabled) + return false; + + return true; +} + +bool kh_Key_waypointsprite_visible_for_player(entity this, entity player, entity view) +{ + if(!kh_tracking_enabled) + return false; + if(!this.owner) + return true; + if(!this.owner.owner) + return true; + return false; // draw only when key is not owned +} + +void kh_update_state() +{ + entity key; + int f; + int s = 0; + FOR_EACH_KH_KEY(key) + { + if(key.owner) + f = key.team; + else + f = 30; + s |= (32 ** key.count) * f; + } + + FOREACH_CLIENT(true, { STAT(KH_KEYS, it) = s; }); + + FOR_EACH_KH_KEY(key) + { + if(key.owner) + STAT(KH_KEYS, key.owner) |= (32 ** key.count) * 31; + } + //print(ftos((nextent(NULL)).kh_state), "\n"); +} + + + + +var kh_Think_t kh_Controller_Thinkfunc; +void kh_Controller_SetThink(float t, kh_Think_t func) // runs occasionaly +{ + kh_Controller_Thinkfunc = func; + kh_controller.cnt = ceil(t); + if(t == 0) + kh_controller.nextthink = time; // force +} +void kh_WaitForPlayers(); +void kh_Controller_Think(entity this) // called a lot +{ + if(game_stopped) + return; + if(this.cnt > 0) + { + if(getthink(this) != kh_WaitForPlayers) + this.cnt -= 1; + } + else if(this.cnt == 0) + { + this.cnt -= 1; + kh_Controller_Thinkfunc(); + } + this.nextthink = time + 1; +} + +// frags f: take from cvar * f +// frags 0: no frags +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(game_stopped) + return; + + if(frags_player) + UpdateFrags(player, frags_player); + + if(key && key.owner && frags_owner) + UpdateFrags(key.owner, frags_owner); + + if(!autocvar_sv_eventlog) //output extra info to the console or text file + return; + + s = strcat(":keyhunt:", what, ":", ftos(player.playerid), ":", ftos(frags_player)); + + if(key && key.owner) + s = strcat(s, ":", ftos(key.owner.playerid)); + else + s = strcat(s, ":0"); + + s = strcat(s, ":", ftos(frags_owner), ":"); + + if(key) + s = strcat(s, key.netname); + + GameLogEcho(s); +} + +vector kh_AttachedOrigin(entity e) // runs when a team captures the flag, it can run 2 or 3 times. +{ + if(e.tag_entity) + { + makevectors(e.tag_entity.angles); + return e.tag_entity.origin + e.origin.x * v_forward - e.origin.y * v_right + e.origin.z * v_up; + } + else + return e.origin; +} + +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 = key.owner.kh_next; + if(key == first) + { + setattachment(key, key.owner, KH_PLAYER_ATTACHMENT_BONE); + if(key.kh_next) + { + setattachment(key.kh_next, key, ""); + setorigin(key, key.kh_next.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST); + setorigin(key.kh_next, KH_PLAYER_ATTACHMENT_DIST_ROTATED); + key.kh_next.angles = '0 0 0'; + } + else + setorigin(key, KH_PLAYER_ATTACHMENT); + key.angles = KH_PLAYER_ATTACHMENT_ANGLES; + } + else + { + setattachment(key, key.kh_prev, ""); + if(key.kh_next) + setattachment(key.kh_next, key, ""); + setorigin(key, KH_PLAYER_ATTACHMENT_DIST_ROTATED); + setorigin(first, first.origin - 0.5 * KH_PLAYER_ATTACHMENT_DIST); + key.angles = '0 0 0'; + } +#else + setattachment(key, key.owner, ""); + setorigin(key, '0 0 1' * KH_KEY_ZSHIFT); // fixing x, y in think + key.angles_y -= key.owner.angles.y; +#endif + key.flags = 0; + 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; + key.nextthink = time; + 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 = key.owner.kh_next; + if(key == first) + { + if(key.kh_next) + { + setattachment(key.kh_next, key.owner, KH_PLAYER_ATTACHMENT_BONE); + setorigin(key.kh_next, key.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST); + key.kh_next.angles = KH_PLAYER_ATTACHMENT_ANGLES; + } + } + else + { + if(key.kh_next) + setattachment(key.kh_next, key.kh_prev, ""); + setorigin(first, first.origin + 0.5 * KH_PLAYER_ATTACHMENT_DIST); + } + // in any case: + setattachment(key, NULL, ""); + 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'); + setattachment(key, NULL, ""); + key.angles_y += key.owner.angles.y; +#endif + key.flags = FL_ITEM; + 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; + key.damageforcescale = autocvar_g_balance_keyhunt_damageforcescale; + key.takedamage = DAMAGE_YES; + // let key.team stay + key.modelindex = kh_key_dropped; + navigation_dynamicgoal_set(key, key.owner); + 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 +{ + if(key.owner == player) + return; + + int ownerteam0 = kh_Key_AllOwnedByWhichTeam(); + + if(key.owner) + { + kh_Key_Detach(key); + + // remove from linked list + if(key.kh_next) + key.kh_next.kh_prev = key.kh_prev; + key.kh_prev.kh_next = key.kh_next; + key.kh_next = NULL; + key.kh_prev = NULL; + + if(key.owner.kh_next == NULL) + { + // No longer a key carrier + if(!kh_no_radar_circles) + WaypointSprite_Ping(key.owner.waypointsprite_attachedforcarrier); + WaypointSprite_DetachCarrier(key.owner); + } + } + + key.owner = player; + + if(player) + { + // insert into linked list + key.kh_next = player.kh_next; + key.kh_prev = player; + player.kh_next = key; + if(key.kh_next) + key.kh_next.kh_prev = key; + + float i; + i = kh_keystatus[key.owner.playerid]; + if(key.netname == "^1red key") + i += 1; + if(key.netname == "^4blue key") + i += 2; + if(key.netname == "^3yellow key") + i += 4; + if(key.netname == "^6pink key") + i += 8; + kh_keystatus[key.owner.playerid] = i; + + kh_Key_Attach(key); + + if(key.kh_next == NULL) + { + // player is now a key carrier + entity wp = WaypointSprite_AttachCarrier(WP_Null, player, RADARICON_FLAGCARRIER); + wp.colormod = colormapPaletteColor(player.team - 1, 0); + player.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = kh_KeyCarrier_waypointsprite_visible_for_player; + WaypointSprite_UpdateRule(player.waypointsprite_attachedforcarrier, player.team, SPRITERULE_TEAMPLAY); + if(player.team == NUM_TEAM_1) + WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierRed, WP_KeyCarrierFriend, WP_KeyCarrierRed); + else if(player.team == NUM_TEAM_2) + WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierBlue, WP_KeyCarrierFriend, WP_KeyCarrierBlue); + else if(player.team == NUM_TEAM_3) + WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierYellow, WP_KeyCarrierFriend, WP_KeyCarrierYellow); + else if(player.team == NUM_TEAM_4) + WaypointSprite_UpdateSprites(player.waypointsprite_attachedforcarrier, WP_KeyCarrierPink, WP_KeyCarrierFriend, WP_KeyCarrierPink); + if(!kh_no_radar_circles) + WaypointSprite_Ping(player.waypointsprite_attachedforcarrier); + } + } + + // moved that here, also update if there's no player + kh_update_state(); + + key.pusher = NULL; + + 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" + FOR_EACH_KH_KEY(k) + { + if (!k.owner) continue; + entity first = WP_Null; + 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, { third = it; break; }); + WaypointSprite_UpdateSprites(k.owner.waypointsprite_attachedforcarrier, first, WP_KeyCarrierFinish, third); + } + } + else + { + kh_interferemsg_time = 0; + + // 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, { first = it; break; }); + entity third = WP_Null; + 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, .entity weaponentity, vector hitloc, vector force) +{ + if(this.owner) + return; + if(ITEM_DAMAGE_NEEDKILL(deathtype)) + { + this.pain_finished = bound(time, time + autocvar_g_balance_keyhunt_delay_damage_return, this.pain_finished); + return; + } + if(force == '0 0 0') + return; + if(time > this.pushltime) + if(IS_PLAYER(attacker)) + this.team = attacker.team; +} + +void kh_Key_Collect(entity key, entity player) //a player picks up a dropped key +{ + sound(player, CH_TRIGGER, SND_KH_COLLECT, VOL_BASE, ATTEN_NORM); + + if(key.kh_dropperteam != player.team) + { + kh_Scores_Event(player, key, "collect", autocvar_g_balance_keyhunt_score_collect, 0); + GameRules_scoring_add(player, KH_PICKUPS, 1); + } + key.kh_dropperteam = 0; + 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(game_stopped) + return; + + if(this.owner) // already carried + return; + + if(ITEM_TOUCH_NEEDKILL()) + { + this.pain_finished = bound(time, time + autocvar_g_balance_keyhunt_delay_damage_return, this.pain_finished); + return; + } + + if (!IS_PLAYER(toucher)) + return; + if(IS_DEAD(toucher)) + return; + if(toucher == this.enemy) + if(time < this.kh_droptime + autocvar_g_balance_keyhunt_delay_collect) + return; // you just dropped it! + kh_Key_Collect(this, toucher); +} + +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 = key.owner; + kh_Key_AssignTo(key, NULL); + if(o) // it was attached + WaypointSprite_Kill(key.waypointsprite_attachedforcarrier); + else // it was dropped + WaypointSprite_DetachCarrier(key); + + // remove key from key list + if (kh_worldkeylist == key) + kh_worldkeylist = kh_worldkeylist.kh_worldkeynext; + else + { + o = kh_worldkeylist; + while (o) + { + if (o.kh_worldkeynext == key) + { + o.kh_worldkeynext = o.kh_worldkeynext.kh_worldkeynext; + break; + } + o = o.kh_worldkeynext; + } + } + + delete(key); + + kh_update_state(); +} + +void kh_FinishRound() // runs when a team captures the keys +{ + // prepare next round + kh_interferemsg_time = 0; + entity key; + + kh_no_radar_circles = true; + FOR_EACH_KH_KEY(key) + kh_Key_Remove(key); + kh_no_radar_circles = false; + + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_KEYHUNT_ROUNDSTART, autocvar_g_balance_keyhunt_delay_round); + kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_round, kh_StartRound); +} + +void nades_GiveBonus(entity player, float score); + +void kh_WinnerTeam(int winner_team) // runs when a team wins +{ + // all key carriers get some points + entity key; + 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 = DistributeEvenly_Get(1); + kh_Scores_Event(key.owner, key, "capture", f, 0); + GameRules_scoring_add_team(key.owner, KH_CAPS, 1); + nades_GiveBonus(key.owner, autocvar_g_nades_bonus_score_high); + } + + bool first = true; + string keyowner = ""; + FOR_EACH_KH_KEY(key) + if(key.owner.kh_next == key) + { + if(!first) + keyowner = strcat(keyowner, ", "); + keyowner = key.owner.netname; + first = false; + } + + 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; + vector firstorigin = '0 0 0', lastorigin = '0 0 0', midpoint = '0 0 0'; + FOR_EACH_KH_KEY(key) + { + vector thisorigin = kh_AttachedOrigin(key); + //dprint("Key origin: ", vtos(thisorigin), "\n"); + midpoint += thisorigin; + + if(!first) + te_lightning2(NULL, lastorigin, thisorigin); + lastorigin = thisorigin; + if(first) + firstorigin = thisorigin; + first = false; + } + if(NumTeams(kh_teams) > 2) + { + te_lightning2(NULL, lastorigin, firstorigin); + } + midpoint = midpoint * (1 / NumTeams(kh_teams)); + 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(int loser_team, entity lostkey) // runs when a player pushes a flag carrier off the map +{ + float f; + entity attacker = NULL; + if(lostkey.pusher) + if(lostkey.pusher.team != loser_team) + if(IS_PLAYER(lostkey.pusher)) + attacker = lostkey.pusher; + + 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); + GameRules_scoring_add(attacker, KH_PUSHES, 1); + //centerprint(attacker, "Your push is the best!"); // does this really need to exist? + } + else + { + int players = 0; + float of = autocvar_g_balance_keyhunt_score_destroyed_ownfactor; + + 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 != loser_team) + ++keys; + + if(lostkey.kh_previous_owner) + kh_Scores_Event(lostkey.kh_previous_owner, NULL, "destroyed", 0, -autocvar_g_balance_keyhunt_score_destroyed); + // don't actually GIVE him the -nn points, just log + + if(lostkey.kh_previous_owner.playerid == lostkey.kh_previous_owner_playerid) + 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 != loser_team) + { + f = DistributeEvenly_Get(of); + kh_Scores_Event(key.owner, NULL, "destroyed_holdingkey", f, 0); + } + + int fragsleft = DistributeEvenly_Get(players); + + // Now distribute these among all other teams... + int j = NumTeams(kh_teams) - 1; + for(int i = 0; i < NumTeams(kh_teams); ++i) + { + 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, { ++players; }); + + DistributeEvenly_Init(fragsleft, j); + fragsleft = DistributeEvenly_Get(j - 1); + DistributeEvenly_Init(DistributeEvenly_Get(1), players); + + FOREACH_CLIENT(IS_PLAYER(it) && it.team == thisteam, { + f = DistributeEvenly_Get(1); + kh_Scores_Event(it, NULL, "destroyed", f, 0); + }); + + --j; + } + } + + 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); + + kh_FinishRound(); +} + +void kh_Key_Think(entity this) // runs all the time +{ + if(game_stopped) + return; + + if(this.owner) + { +#ifndef KH_PLAYER_USE_ATTACHMENT + makevectors('0 1 0' * (this.cnt + (time % 360) * KH_KEY_XYSPEED)); + setorigin(this, v_forward * KH_KEY_XYDIST + '0 0 1' * this.origin.z); +#endif + } + + // if in nodrop or time over, end the round + if(!this.owner) + if(time > this.pain_finished) + kh_LoserTeam(this.team, this); + + if(this.owner) + if(kh_Key_AllOwnedByWhichTeam() != -1) + { + if(this.siren_time < time) + { + sound(this.owner, CH_TRIGGER, SND_KH_ALARM, VOL_BASE, ATTEN_NORM); // play a simple alarm + this.siren_time = time + 2.5; // repeat every 2.5 seconds + } + + entity key; + vector p = this.owner.origin; + FOR_EACH_KH_KEY(key) + if(vdist(key.owner.origin - p, >, autocvar_g_balance_keyhunt_maxdist)) + goto not_winning; + kh_WinnerTeam(this.team); +LABEL(not_winning) + } + + if(kh_interferemsg_time && time > kh_interferemsg_time) + { + kh_interferemsg_time = 0; + 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); + else + 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; +} + +void key_reset(entity this) +{ + kh_Key_AssignTo(this, NULL); + kh_Key_Remove(this); +} + +const string STR_ITEM_KH_KEY = "item_kh_key"; +void kh_Key_Spawn(entity initial_owner, float _angle, float i) // runs every time a new flag is created, ie after all the keys have been collected +{ + entity key = spawn(); + key.count = i; + key.classname = STR_ITEM_KH_KEY; + settouch(key, kh_Key_Touch); + setthink(key, kh_Key_Think); + key.nextthink = time; + key.items = IT_KEY1 | IT_KEY2; + key.cnt = _angle; + 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; + key.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_PLAYERCLIP | DPCONTENTS_BOTCLIP; + 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) + { + case NUM_TEAM_1: + key.netname = "^1red key"; + break; + case NUM_TEAM_2: + key.netname = "^4blue key"; + break; + case NUM_TEAM_3: + key.netname = "^3yellow key"; + break; + case NUM_TEAM_4: + key.netname = "^6pink key"; + break; + default: + key.netname = "NETGIER key"; + break; + } + + // link into key list + key.kh_worldkeynext = kh_worldkeylist; + kh_worldkeylist = key; + + Send_Notification(NOTIF_ONE, initial_owner, MSG_CENTER, APP_TEAM_NUM(initial_owner.team, CENTER_KEYHUNT_START)); + + WaypointSprite_Spawn(WP_KeyDropped, 0, 0, key, '0 0 1' * KH_KEY_WP_ZSHIFT, NULL, key.team, key, waypointsprite_attachedforcarrier, false, RADARICON_FLAG); + key.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = kh_Key_waypointsprite_visible_for_player; + + kh_Key_AssignTo(key, initial_owner); +} + +// -1 when no team completely owns all keys yet +int kh_Key_AllOwnedByWhichTeam() // constantly called. check to see if all the keys are owned by the same team +{ + entity key; + int teem = -1; + int keys = NumTeams(kh_teams); + FOR_EACH_KH_KEY(key) + { + if(!key.owner) + return -1; + if(teem == -1) + teem = key.team; + else if(teem != key.team) + return -1; + --keys; + } + if(keys != 0) + return -1; + return teem; +} + +void kh_Key_DropOne(entity key) +{ + // prevent collecting this one for some time + entity player = key.owner; + + key.kh_droptime = time; + key.enemy = player; + + kh_Scores_Event(player, key, "dropkey", 0, 0); + 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); + key.velocity = W_CalculateProjectileVelocity(player, player.velocity, autocvar_g_balance_keyhunt_throwvelocity * v_forward, false); + key.pusher = NULL; + key.pushltime = time + autocvar_g_balance_keyhunt_protecttime; + key.kh_dropperteam = key.team; + + sound(player, CH_TRIGGER, SND_KH_DROP, VOL_BASE, ATTEN_NORM); +} + +void kh_Key_DropAll(entity player, float suicide) // runs whenever a player dies +{ + if(player.kh_next) + { + 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); + 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); + key.pusher = mypusher; + key.pushltime = time + autocvar_g_balance_keyhunt_protecttime; + if(suicide) + key.kh_dropperteam = player.team; + } + sound(player, CH_TRIGGER, SND_KH_DROP, VOL_BASE, ATTEN_NORM); + } +} + +int kh_GetMissingTeams() +{ + int missing_teams = 0; + for(int i = 0; i < NumTeams(kh_teams); ++i) + { + int teem = kh_Team_ByID(i); + int players = 0; + FOREACH_CLIENT(IS_PLAYER(it), { + if(!IS_DEAD(it) && !PHYS_INPUT_BUTTON_CHAT(it) && it.team == teem) + ++players; + }); + if (!players) + 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; + } + + int missing_teams_mask = kh_GetMissingTeams(); + if(!missing_teams_mask) + { + if(prev_missing_teams_mask > 0) + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS); + prev_missing_teams_mask = -1; + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_KEYHUNT_ROUNDSTART, autocvar_g_balance_keyhunt_delay_round); + kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_round, kh_StartRound); + } + else + { + if(player_count == 0) + { + if(prev_missing_teams_mask > 0) + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_TEAMS); + prev_missing_teams_mask = -1; + } + else + { + if(prev_missing_teams_mask != missing_teams_mask) + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_TEAMS, missing_teams_mask); + prev_missing_teams_mask = missing_teams_mask; + } + } + kh_Controller_SetThink(1, kh_WaitForPlayers); + } +} + +void kh_EnableTrackingDevice() // runs after each round +{ + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_KEYHUNT); + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_KEYHUNT_OTHER); + + kh_tracking_enabled = true; +} + +void kh_StartRound() // runs at the start of each round +{ + if(time < game_starttime) + { + kh_Controller_SetThink(game_starttime - time + 0.1, kh_WaitForPlayers); + return; + } + + if(kh_GetMissingTeams()) + { + kh_Controller_SetThink(1, kh_WaitForPlayers); + return; + } + + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_KEYHUNT); + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_KEYHUNT_OTHER); + + for(int i = 0; i < NumTeams(kh_teams); ++i) + { + int teem = kh_Team_ByID(i); + int players = 0; + entity my_player = NULL; + 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); + } + + kh_tracking_enabled = false; + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_KEYHUNT_SCAN, autocvar_g_balance_keyhunt_delay_tracking); + kh_Controller_SetThink(autocvar_g_balance_keyhunt_delay_tracking, kh_EnableTrackingDevice); +} + +float kh_HandleFrags(entity attacker, entity targ, float f) // adds to the player score +{ + if(attacker == targ) + return f; + + if(targ.kh_next) + { + if(attacker.team == targ.team) + { + 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); + GameRules_scoring_add(attacker, KH_KCKILLS, 1); + // the frag gets added later + } + } + + return f; +} + +void kh_Initialize() // sets up th KH environment +{ + // setup variables + kh_teams = autocvar_g_keyhunt_teams_override; + if(kh_teams < 2) + 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(); + setthink(kh_controller, kh_Controller_Think); + kh_Controller_SetThink(0, kh_WaitForPlayers); + + setmodel(kh_controller, MDL_KH_KEY); + kh_key_dropped = kh_controller.modelindex; + /* + dprint(vtos(kh_controller.mins)); + dprint(vtos(kh_controller.maxs)); + dprint("\n"); + */ +#ifdef KH_PLAYER_USE_CARRIEDMODEL + setmodel(kh_controller, MDL_KH_KEY_CARRIED); + kh_key_carried = kh_controller.modelindex; +#else + kh_key_carried = kh_key_dropped; +#endif + + kh_controller.model = ""; + kh_controller.modelindex = 0; + + kh_ScoreRules(kh_teams); +} + +void kh_finalize() +{ + // to be called before intermission + kh_FinishRound(); + delete(kh_controller); + kh_controller = NULL; +} + +// legacy bot role + +void(entity this) havocbot_role_kh_carrier; +void(entity this) havocbot_role_kh_defense; +void(entity this) havocbot_role_kh_offense; +void(entity this) havocbot_role_kh_freelancer; + + +void havocbot_goalrating_kh(entity this, float ratingscale_team, float ratingscale_dropped, float ratingscale_enemy) +{ + entity head; + for (head = kh_worldkeylist; head; head = head.kh_worldkeynext) + { + if(head.owner == this) + continue; + if(!kh_tracking_enabled) + { + // if it's carried by our team we know about it + // otherwise we have to see it to know about it + if(!head.owner || head.team != this.team) + { + traceline(this.origin + this.view_ofs, head.origin, MOVE_NOMONSTERS, this); + if (trace_fraction < 1 && trace_ent != head) + continue; // skip what I can't see + } + } + if(!head.owner) + navigation_routerating(this, head, ratingscale_dropped * 10000, 100000); + else if(head.team == this.team) + navigation_routerating(this, head.owner, ratingscale_team * 10000, 100000); + else + navigation_routerating(this, head.owner, ratingscale_enemy * 10000, 100000); + } + + havocbot_goalrating_items(this, 1, this.origin, 10000); +} + +void havocbot_role_kh_carrier(entity this) +{ + if(IS_DEAD(this)) + return; + + if (!(this.kh_next)) + { + LOG_TRACE("changing role to freelancer"); + this.havocbot_role = havocbot_role_kh_freelancer; + this.havocbot_role_timeout = 0; + return; + } + + if (navigation_goalrating_timeout(this)) + { + navigation_goalrating_start(this); + + if(kh_Key_AllOwnedByWhichTeam() == this.team) + havocbot_goalrating_kh(this, 10, 0.1, 0.05); // bring home + else + havocbot_goalrating_kh(this, 4, 4, 0.5); // play defensively + + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); + } +} + +void havocbot_role_kh_defense(entity this) +{ + if(IS_DEAD(this)) + return; + + if (this.kh_next) + { + LOG_TRACE("changing role to carrier"); + this.havocbot_role = havocbot_role_kh_carrier; + this.havocbot_role_timeout = 0; + return; + } + + if (!this.havocbot_role_timeout) + this.havocbot_role_timeout = time + random() * 10 + 20; + if (time > this.havocbot_role_timeout) + { + LOG_TRACE("changing role to freelancer"); + this.havocbot_role = havocbot_role_kh_freelancer; + this.havocbot_role_timeout = 0; + return; + } + + if (navigation_goalrating_timeout(this)) + { + float key_owner_team; + navigation_goalrating_start(this); + + key_owner_team = kh_Key_AllOwnedByWhichTeam(); + if(key_owner_team == this.team) + havocbot_goalrating_kh(this, 10, 0.1, 0.05); // defend key carriers + else if(key_owner_team == -1) + havocbot_goalrating_kh(this, 4, 1, 0.05); // play defensively + else + havocbot_goalrating_kh(this, 0.1, 0.1, 5); // ATTACK ANYWAY + + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); + } +} + +void havocbot_role_kh_offense(entity this) +{ + if(IS_DEAD(this)) + return; + + if (this.kh_next) + { + LOG_TRACE("changing role to carrier"); + this.havocbot_role = havocbot_role_kh_carrier; + this.havocbot_role_timeout = 0; + return; + } + + if (!this.havocbot_role_timeout) + this.havocbot_role_timeout = time + random() * 10 + 20; + if (time > this.havocbot_role_timeout) + { + LOG_TRACE("changing role to freelancer"); + this.havocbot_role = havocbot_role_kh_freelancer; + this.havocbot_role_timeout = 0; + return; + } + + if (navigation_goalrating_timeout(this)) + { + float key_owner_team; + + navigation_goalrating_start(this); + + key_owner_team = kh_Key_AllOwnedByWhichTeam(); + if(key_owner_team == this.team) + havocbot_goalrating_kh(this, 10, 0.1, 0.05); // defend anyway + else if(key_owner_team == -1) + havocbot_goalrating_kh(this, 0.1, 1, 2); // play offensively + else + havocbot_goalrating_kh(this, 0.1, 0.1, 5); // ATTACK! EMERGENCY! + + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); + } +} + +void havocbot_role_kh_freelancer(entity this) +{ + if(IS_DEAD(this)) + return; + + if (this.kh_next) + { + LOG_TRACE("changing role to carrier"); + this.havocbot_role = havocbot_role_kh_carrier; + this.havocbot_role_timeout = 0; + return; + } + + if (!this.havocbot_role_timeout) + this.havocbot_role_timeout = time + random() * 10 + 10; + if (time > this.havocbot_role_timeout) + { + if (random() < 0.5) + { + LOG_TRACE("changing role to offense"); + this.havocbot_role = havocbot_role_kh_offense; + } + else + { + LOG_TRACE("changing role to defense"); + this.havocbot_role = havocbot_role_kh_defense; + } + this.havocbot_role_timeout = 0; + return; + } + + if (navigation_goalrating_timeout(this)) + { + navigation_goalrating_start(this); + + int key_owner_team = kh_Key_AllOwnedByWhichTeam(); + if(key_owner_team == this.team) + havocbot_goalrating_kh(this, 10, 0.1, 0.05); // defend anyway + else if(key_owner_team == -1) + havocbot_goalrating_kh(this, 1, 10, 2); // prefer dropped keys + else + havocbot_goalrating_kh(this, 0.1, 0.1, 5); // ATTACK ANYWAY + + navigation_goalrating_end(this); + + navigation_goalrating_timeout_set(this); + } +} + + +// register this as a mutator + +MUTATOR_HOOKFUNCTION(kh, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + kh_Key_DropAll(player, true); +} + +MUTATOR_HOOKFUNCTION(kh, MakePlayerObserver) +{ + entity player = M_ARGV(0, entity); + + kh_Key_DropAll(player, true); +} + +MUTATOR_HOOKFUNCTION(kh, PlayerDies) +{ + entity frag_attacker = M_ARGV(1, entity); + entity frag_target = M_ARGV(2, entity); + + if(frag_target == frag_attacker) + kh_Key_DropAll(frag_target, true); + else if(IS_PLAYER(frag_attacker)) + kh_Key_DropAll(frag_target, false); + else + kh_Key_DropAll(frag_target, true); +} + +MUTATOR_HOOKFUNCTION(kh, GiveFragsForKill, CBC_ORDER_FIRST) +{ + entity frag_attacker = M_ARGV(0, entity); + entity frag_target = M_ARGV(1, entity); + float frag_score = M_ARGV(2, float); + M_ARGV(2, float) = kh_HandleFrags(frag_attacker, frag_target, frag_score); +} + +MUTATOR_HOOKFUNCTION(kh, MatchEnd) +{ + kh_finalize(); +} + +MUTATOR_HOOKFUNCTION(kh, TeamBalance_CheckAllowedTeams, CBC_ORDER_EXCLUSIVE) +{ + M_ARGV(0, float) = kh_teams; + return true; +} + +MUTATOR_HOOKFUNCTION(kh, SpectateCopy) +{ + entity spectatee = M_ARGV(0, entity); + entity client = M_ARGV(1, entity); + + STAT(KH_KEYS, client) = STAT(KH_KEYS, spectatee); +} + +MUTATOR_HOOKFUNCTION(kh, PlayerUseKey) +{ + entity player = M_ARGV(0, entity); + + if(MUTATOR_RETURNVALUE == 0) + { + entity k = player.kh_next; + if(k) + { + kh_Key_DropOne(k); + return true; + } + } +} + +MUTATOR_HOOKFUNCTION(kh, HavocBot_ChooseRole) +{ + entity bot = M_ARGV(0, entity); + + if(IS_DEAD(bot)) + return true; + + float r = random() * 3; + if (r < 1) + bot.havocbot_role = havocbot_role_kh_offense; + else if (r < 2) + bot.havocbot_role = havocbot_role_kh_defense; + else + bot.havocbot_role = havocbot_role_kh_freelancer; + + return true; +} + +MUTATOR_HOOKFUNCTION(kh, DropSpecialItems) +{ + entity frag_target = M_ARGV(0, entity); + + kh_Key_DropAll(frag_target, false); +} + +MUTATOR_HOOKFUNCTION(kh, reset_map_global) +{ + kh_WaitForPlayers(); // takes care of killing the "missing teams" message +}