0d46dcaa472868f4a7a86590515ca5b106c4043e
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_keepaway.qc
1 #include "gamemode_keepaway.qh"
2 #ifndef GAMEMODE_KEEPAWAY_H
3 #define GAMEMODE_KEEPAWAY_H
4
5 void ka_Initialize();
6
7 REGISTER_MUTATOR(ka, false)
8 {
9         MUTATOR_ONADD
10         {
11                 if (time > 1) // game loads at time 1
12                         error("This is a game type and it cannot be added at runtime.");
13                 ka_Initialize();
14         }
15
16         MUTATOR_ONROLLBACK_OR_REMOVE
17         {
18                 // we actually cannot roll back ka_Initialize here
19                 // BUT: we don't need to! If this gets called, adding always
20                 // succeeds.
21         }
22
23         MUTATOR_ONREMOVE
24         {
25                 LOG_INFO("This is a game type and it cannot be removed at runtime.");
26                 return -1;
27         }
28
29         return 0;
30 }
31
32
33 entity ka_ball;
34
35 const float SP_KEEPAWAY_PICKUPS = 4;
36 const float SP_KEEPAWAY_CARRIERKILLS = 5;
37 const float SP_KEEPAWAY_BCTIME = 6;
38
39 void(entity this) havocbot_role_ka_carrier;
40 void(entity this) havocbot_role_ka_collector;
41
42 void ka_DropEvent(entity plyr);
43 #endif
44
45 #ifdef IMPLEMENTATION
46
47 int autocvar_g_keepaway_ballcarrier_effects;
48 float autocvar_g_keepaway_ballcarrier_damage;
49 float autocvar_g_keepaway_ballcarrier_force;
50 float autocvar_g_keepaway_ballcarrier_highspeed;
51 float autocvar_g_keepaway_ballcarrier_selfdamage;
52 float autocvar_g_keepaway_ballcarrier_selfforce;
53 float autocvar_g_keepaway_noncarrier_damage;
54 float autocvar_g_keepaway_noncarrier_force;
55 float autocvar_g_keepaway_noncarrier_selfdamage;
56 float autocvar_g_keepaway_noncarrier_selfforce;
57 bool autocvar_g_keepaway_noncarrier_warn;
58 int autocvar_g_keepaway_score_bckill;
59 int autocvar_g_keepaway_score_killac;
60 int autocvar_g_keepaway_score_timepoints;
61 float autocvar_g_keepaway_score_timeinterval;
62 float autocvar_g_keepawayball_damageforcescale;
63 int autocvar_g_keepawayball_effects;
64 float autocvar_g_keepawayball_respawntime;
65 int autocvar_g_keepawayball_trail_color;
66
67 bool ka_ballcarrier_waypointsprite_visible_for_player(entity this, entity player, entity view) // runs on waypoints which are attached to ballcarriers, updates once per frame
68 {
69         if(view.ballcarried)
70                 if(IS_SPEC(player))
71                         return false; // we don't want spectators of the ballcarrier to see the attached waypoint on the top of their screen
72
73         // TODO: Make the ballcarrier lack a waypointsprite whenever they have the invisibility powerup
74
75         return true;
76 }
77
78 void ka_EventLog(string mode, entity actor) // use an alias for easy changing and quick editing later
79 {
80         if(autocvar_sv_eventlog)
81                 GameLogEcho(strcat(":ka:", mode, ((actor != world) ? (strcat(":", ftos(actor.playerid))) : "")));
82 }
83
84 void ka_TouchEvent();
85 void ka_RespawnBall(entity this);
86 void ka_RespawnBall_self() { SELFPARAM(); ka_RespawnBall(this); }
87 void ka_RespawnBall(entity this) // runs whenever the ball needs to be relocated
88 {
89         if(gameover) { return; }
90         vector oldballorigin = self.origin;
91
92         if(!MoveToRandomMapLocation(self, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
93         {
94                 entity spot = SelectSpawnPoint(this, true);
95                 setorigin(self, spot.origin);
96                 self.angles = spot.angles;
97         }
98
99         makevectors(self.angles);
100         self.movetype = MOVETYPE_BOUNCE;
101         self.velocity = '0 0 200';
102         self.angles = '0 0 0';
103         self.effects = autocvar_g_keepawayball_effects;
104         settouch(self, ka_TouchEvent);
105         setthink(self, ka_RespawnBall_self);
106         self.nextthink = time + autocvar_g_keepawayball_respawntime;
107
108         Send_Effect(EFFECT_ELECTRO_COMBO, oldballorigin, '0 0 0', 1);
109         Send_Effect(EFFECT_ELECTRO_COMBO, self.origin, '0 0 0', 1);
110
111         WaypointSprite_Spawn(WP_KaBall, 0, 0, self, '0 0 64', world, self.team, self, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER);
112         WaypointSprite_Ping(self.waypointsprite_attachedforcarrier);
113
114         sound(self, CH_TRIGGER, SND_KA_RESPAWN, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
115 }
116
117 void ka_TimeScoring()
118 {SELFPARAM();
119         if(self.owner.ballcarried)
120         { // add points for holding the ball after a certain amount of time
121                 if(autocvar_g_keepaway_score_timepoints)
122                         PlayerScore_Add(self.owner, SP_SCORE, autocvar_g_keepaway_score_timepoints);
123
124                 PlayerScore_Add(self.owner, SP_KEEPAWAY_BCTIME, (autocvar_g_keepaway_score_timeinterval / 1)); // interval is divided by 1 so that time always shows "seconds"
125                 self.nextthink = time + autocvar_g_keepaway_score_timeinterval;
126         }
127 }
128
129 void ka_TouchEvent() // runs any time that the ball comes in contact with something
130 {SELFPARAM();
131         if(gameover) { return; }
132         if(!self) { return; }
133         if(trace_dphitq3surfaceflags & Q3SURFACEFLAG_NOIMPACT)
134         { // The ball fell off the map, respawn it since players can't get to it
135                 ka_RespawnBall(self);
136                 return;
137         }
138         if(IS_DEAD(other)) { return; }
139         if(STAT(FROZEN, other)) { return; }
140         if (!IS_PLAYER(other))
141         {  // The ball just touched an object, most likely the world
142                 Send_Effect(EFFECT_BALL_SPARKS, self.origin, '0 0 0', 1);
143                 sound(self, CH_TRIGGER, SND_KA_TOUCH, VOL_BASE, ATTEN_NORM);
144                 return;
145         }
146         else if(self.wait > time) { return; }
147
148         // attach the ball to the player
149         self.owner = other;
150         other.ballcarried = self;
151         setattachment(self, other, "");
152         setorigin(self, '0 0 0');
153
154         // make the ball invisible/unable to do anything/set up time scoring
155         self.velocity = '0 0 0';
156         self.movetype = MOVETYPE_NONE;
157         self.effects |= EF_NODRAW;
158         settouch(self, func_null);
159         setthink(self, ka_TimeScoring);
160         self.nextthink = time + autocvar_g_keepaway_score_timeinterval;
161         self.takedamage = DAMAGE_NO;
162
163         // apply effects to player
164         other.glow_color = autocvar_g_keepawayball_trail_color;
165         other.glow_trail = true;
166         other.effects |= autocvar_g_keepaway_ballcarrier_effects;
167
168         // messages and sounds
169         ka_EventLog("pickup", other);
170         Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_KEEPAWAY_PICKUP, other.netname);
171         Send_Notification(NOTIF_ALL_EXCEPT, other, MSG_CENTER, CENTER_KEEPAWAY_PICKUP, other.netname);
172         Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_KEEPAWAY_PICKUP_SELF);
173         sound(self.owner, CH_TRIGGER, SND_KA_PICKEDUP, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
174
175         // scoring
176         PlayerScore_Add(other, SP_KEEPAWAY_PICKUPS, 1);
177
178         // waypoints
179         WaypointSprite_AttachCarrier(WP_KaBallCarrier, other, RADARICON_FLAGCARRIER);
180         other.waypointsprite_attachedforcarrier.waypointsprite_visible_for_player = ka_ballcarrier_waypointsprite_visible_for_player;
181         WaypointSprite_UpdateRule(other.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
182         WaypointSprite_Ping(other.waypointsprite_attachedforcarrier);
183         WaypointSprite_Kill(self.waypointsprite_attachedforcarrier);
184 }
185
186 void ka_DropEvent(entity plyr) // runs any time that a player is supposed to lose the ball
187 {
188         entity ball;
189         ball = plyr.ballcarried;
190
191         if(!ball) { return; }
192
193         // reset the ball
194         setattachment(ball, world, "");
195         ball.movetype = MOVETYPE_BOUNCE;
196         ball.wait = time + 1;
197         settouch(ball, ka_TouchEvent);
198         setthink(ball, ka_RespawnBall_self);
199         ball.nextthink = time + autocvar_g_keepawayball_respawntime;
200         ball.takedamage = DAMAGE_YES;
201         ball.effects &= ~EF_NODRAW;
202         setorigin(ball, plyr.origin + '0 0 10');
203         ball.velocity = '0 0 200' + '0 100 0'*crandom() + '100 0 0'*crandom();
204         ball.owner.ballcarried = world; // I hope nothing checks to see if the world has the ball in the rest of my code :P
205         ball.owner = world;
206
207         // reset the player effects
208         plyr.glow_trail = false;
209         plyr.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
210
211         // messages and sounds
212         ka_EventLog("dropped", plyr);
213         Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_KEEPAWAY_DROPPED, plyr.netname);
214         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_KEEPAWAY_DROPPED, plyr.netname);
215         sound(other, CH_TRIGGER, SND_KA_DROPPED, VOL_BASE, ATTEN_NONE); // ATTEN_NONE (it's a sound intended to be heard anywhere)
216
217         // scoring
218         // PlayerScore_Add(plyr, SP_KEEPAWAY_DROPS, 1); Not anymore, this is 100% the same as pickups and is useless.
219
220         // waypoints
221         WaypointSprite_Spawn(WP_KaBall, 0, 0, ball, '0 0 64', world, ball.team, ball, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER);
222         WaypointSprite_UpdateRule(ball.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
223         WaypointSprite_Ping(ball.waypointsprite_attachedforcarrier);
224         WaypointSprite_Kill(plyr.waypointsprite_attachedforcarrier);
225 }
226
227 /** used to clear the ballcarrier whenever the match switches from warmup to normal */
228 void ka_Reset(entity this)
229 {
230         if((this.owner) && (IS_PLAYER(this.owner)))
231                 ka_DropEvent(this.owner);
232
233         if(time < game_starttime)
234         {
235                 setthink(this, ka_RespawnBall_self);
236                 settouch(this, func_null);
237                 this.nextthink = game_starttime;
238         }
239         else
240                 ka_RespawnBall(this);
241 }
242
243
244 // ================
245 // Bot player logic
246 // ================
247
248 void havocbot_goalrating_ball(entity this, float ratingscale, vector org)
249 {
250         float t;
251         entity ball_owner;
252         ball_owner = ka_ball.owner;
253
254         if (ball_owner == this)
255                 return;
256
257         // If ball is carried by player then hunt them down.
258         if (ball_owner)
259         {
260                 t = (this.health + this.armorvalue) / (ball_owner.health + ball_owner.armorvalue);
261                 navigation_routerating(this, ball_owner, t * ratingscale, 2000);
262         }
263         else // Ball has been dropped so collect.
264                 navigation_routerating(this, ka_ball, ratingscale, 2000);
265 }
266
267 void havocbot_role_ka_carrier(entity this)
268 {
269         if (IS_DEAD(this))
270                 return;
271
272         if (time > this.bot_strategytime)
273         {
274                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
275
276                 navigation_goalrating_start(this);
277                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
278                 havocbot_goalrating_enemyplayers(this, 20000, this.origin, 10000);
279                 //havocbot_goalrating_waypoints(1, this.origin, 1000);
280                 navigation_goalrating_end(this);
281         }
282
283         if (!this.ballcarried)
284         {
285                 this.havocbot_role = havocbot_role_ka_collector;
286                 this.bot_strategytime = 0;
287         }
288 }
289
290 void havocbot_role_ka_collector(entity this)
291 {
292         if (IS_DEAD(this))
293                 return;
294
295         if (time > this.bot_strategytime)
296         {
297                 this.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
298
299                 navigation_goalrating_start(this);
300                 havocbot_goalrating_items(this, 10000, this.origin, 10000);
301                 havocbot_goalrating_enemyplayers(this, 1000, this.origin, 10000);
302                 havocbot_goalrating_ball(this, 20000, this.origin);
303                 navigation_goalrating_end(this);
304         }
305
306         if (this.ballcarried)
307         {
308                 this.havocbot_role = havocbot_role_ka_carrier;
309                 this.bot_strategytime = 0;
310         }
311 }
312
313
314 // ==============
315 // Hook Functions
316 // ==============
317
318 MUTATOR_HOOKFUNCTION(ka, PlayerDies)
319 {
320         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)))
321         {
322                 if(frag_target.ballcarried) { // add to amount of times killing carrier
323                         PlayerScore_Add(frag_attacker, SP_KEEPAWAY_CARRIERKILLS, 1);
324                         if(autocvar_g_keepaway_score_bckill) // add bckills to the score
325                                 PlayerScore_Add(frag_attacker, SP_SCORE, autocvar_g_keepaway_score_bckill);
326                 }
327                 else if(!frag_attacker.ballcarried)
328                         if(autocvar_g_keepaway_noncarrier_warn)
329                                 Send_Notification(NOTIF_ONE_ONLY, frag_attacker, MSG_CENTER, CENTER_KEEPAWAY_WARN);
330
331                 if(frag_attacker.ballcarried) // add to amount of kills while ballcarrier
332                         PlayerScore_Add(frag_attacker, SP_SCORE, autocvar_g_keepaway_score_killac);
333         }
334
335         if(frag_target.ballcarried) { ka_DropEvent(frag_target); } // a player with the ball has died, drop it
336         return 0;
337 }
338
339 MUTATOR_HOOKFUNCTION(ka, GiveFragsForKill)
340 {
341         frag_score = 0; // no frags counted in keepaway
342         return 1; // you deceptive little bugger ;3 This needs to be true in order for this function to even count.
343 }
344
345 MUTATOR_HOOKFUNCTION(ka, PlayerPreThink)
346 {SELFPARAM();
347         // clear the item used for the ball in keepaway
348         self.items &= ~IT_KEY1;
349
350         // if the player has the ball, make sure they have the item for it (Used for HUD primarily)
351         if(self.ballcarried)
352                 self.items |= IT_KEY1;
353
354         return 0;
355 }
356
357 MUTATOR_HOOKFUNCTION(ka, PlayerUseKey)
358 {SELFPARAM();
359         if(MUTATOR_RETURNVALUE == 0)
360         if(self.ballcarried)
361         {
362                 ka_DropEvent(self);
363                 return 1;
364         }
365         return 0;
366 }
367
368 MUTATOR_HOOKFUNCTION(ka, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
369 {
370         if(frag_attacker.ballcarried) // if the attacker is a ballcarrier
371         {
372                 if(frag_target == frag_attacker) // damage done to yourself
373                 {
374                         frag_damage *= autocvar_g_keepaway_ballcarrier_selfdamage;
375                         frag_force *= autocvar_g_keepaway_ballcarrier_selfforce;
376                 }
377                 else // damage done to noncarriers
378                 {
379                         frag_damage *= autocvar_g_keepaway_ballcarrier_damage;
380                         frag_force *= autocvar_g_keepaway_ballcarrier_force;
381                 }
382         }
383         else if (!frag_target.ballcarried) // if the target is a noncarrier
384         {
385                 if(frag_target == frag_attacker) // damage done to yourself
386                 {
387                         frag_damage *= autocvar_g_keepaway_noncarrier_selfdamage;
388                         frag_force *= autocvar_g_keepaway_noncarrier_selfforce;
389                 }
390                 else // damage done to other noncarriers
391                 {
392                         frag_damage *= autocvar_g_keepaway_noncarrier_damage;
393                         frag_force *= autocvar_g_keepaway_noncarrier_force;
394                 }
395         }
396         return 0;
397 }
398
399 MUTATOR_HOOKFUNCTION(ka, ClientDisconnect)
400 {SELFPARAM();
401         if(self.ballcarried) { ka_DropEvent(self); } // a player with the ball has left the match, drop it
402         return 0;
403 }
404
405 MUTATOR_HOOKFUNCTION(ka, MakePlayerObserver)
406 {SELFPARAM();
407         if(self.ballcarried) { ka_DropEvent(self); } // a player with the ball has left the match, drop it
408         return 0;
409 }
410
411 MUTATOR_HOOKFUNCTION(ka, PlayerPowerups)
412 {SELFPARAM();
413         // In the future this hook is supposed to allow me to do some extra stuff with waypointsprites and invisibility powerup
414         // So bare with me until I can fix a certain bug with ka_ballcarrier_waypointsprite_visible_for_player()
415
416         self.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
417
418         if(self.ballcarried)
419                 self.effects |= autocvar_g_keepaway_ballcarrier_effects;
420
421         return 0;
422 }
423
424 .float stat_sv_airspeedlimit_nonqw;
425 .float stat_sv_maxspeed;
426
427 MUTATOR_HOOKFUNCTION(ka, PlayerPhysics)
428 {SELFPARAM();
429         if(self.ballcarried)
430         {
431                 self.stat_sv_airspeedlimit_nonqw *= autocvar_g_keepaway_ballcarrier_highspeed;
432                 self.stat_sv_maxspeed *= autocvar_g_keepaway_ballcarrier_highspeed;
433         }
434         return false;
435 }
436
437 MUTATOR_HOOKFUNCTION(ka, BotShouldAttack)
438 {SELFPARAM();
439         // if neither player has ball then don't attack unless the ball is on the ground
440         if(!checkentity.ballcarried && !self.ballcarried && ka_ball.owner)
441                 return true;
442         return false;
443 }
444
445 MUTATOR_HOOKFUNCTION(ka, HavocBot_ChooseRole)
446 {SELFPARAM();
447         if (self.ballcarried)
448                 self.havocbot_role = havocbot_role_ka_carrier;
449         else
450                 self.havocbot_role = havocbot_role_ka_collector;
451         return true;
452 }
453
454 MUTATOR_HOOKFUNCTION(ka, DropSpecialItems)
455 {
456         if(frag_target.ballcarried)
457                 ka_DropEvent(frag_target);
458
459         return false;
460 }
461
462
463 // ==============
464 // Initialization
465 // ==============
466
467 void ka_SpawnBall() // loads various values for the ball, runs only once at start of match
468 {
469         entity e = new(keepawayball);
470         e.model = "models/orbs/orbblue.md3";
471         precache_model(e.model);
472         _setmodel(e, e.model);
473         setsize(e, '-16 -16 -20', '16 16 20'); // 20 20 20 was too big, player is only 16 16 24... gotta cheat with the Z (20) axis so that the particle isn't cut off
474         e.damageforcescale = autocvar_g_keepawayball_damageforcescale;
475         e.takedamage = DAMAGE_YES;
476         e.solid = SOLID_TRIGGER;
477         e.movetype = MOVETYPE_BOUNCE;
478         e.glow_color = autocvar_g_keepawayball_trail_color;
479         e.glow_trail = true;
480         e.flags = FL_ITEM;
481         e.pushable = true;
482         e.reset = ka_Reset;
483         settouch(e, ka_TouchEvent);
484         e.owner = world;
485         ka_ball = e;
486
487         InitializeEntity(e, ka_RespawnBall, INITPRIO_SETLOCATION); // is this the right priority? Neh, I have no idea.. Well-- it works! So.
488 }
489
490 void ka_ScoreRules()
491 {
492         ScoreRules_basics(0, SFL_SORT_PRIO_PRIMARY, 0, true); // SFL_SORT_PRIO_PRIMARY
493         ScoreInfo_SetLabel_PlayerScore(SP_KEEPAWAY_PICKUPS,                     "pickups",              0);
494         ScoreInfo_SetLabel_PlayerScore(SP_KEEPAWAY_CARRIERKILLS,        "bckills",              0);
495         ScoreInfo_SetLabel_PlayerScore(SP_KEEPAWAY_BCTIME,                      "bctime",               SFL_SORT_PRIO_SECONDARY);
496         ScoreRules_basics_end();
497 }
498
499 void ka_Initialize() // run at the start of a match, initiates game mode
500 {
501         ka_ScoreRules();
502         ka_SpawnBall();
503 }
504
505 #endif