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