Remove SELFPARAM() from .think and .touch
[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(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 = self.origin;
90
91         if(!MoveToRandomMapLocation(self, 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(self, spot.origin);
95                 self.angles = spot.angles;
96         }
97
98         makevectors(self.angles);
99         self.movetype = MOVETYPE_BOUNCE;
100         self.velocity = '0 0 200';
101         self.angles = '0 0 0';
102         self.effects = autocvar_g_keepawayball_effects;
103         settouch(self, ka_TouchEvent);
104         setthink(self, ka_RespawnBall);
105         self.nextthink = time + autocvar_g_keepawayball_respawntime;
106
107         Send_Effect(EFFECT_ELECTRO_COMBO, oldballorigin, '0 0 0', 1);
108         Send_Effect(EFFECT_ELECTRO_COMBO, self.origin, '0 0 0', 1);
109
110         WaypointSprite_Spawn(WP_KaBall, 0, 0, self, '0 0 64', world, self.team, self, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER);
111         WaypointSprite_Ping(self.waypointsprite_attachedforcarrier);
112
113         sound(self, 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(self.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(self.owner, SP_SCORE, autocvar_g_keepaway_score_timepoints);
122
123                 PlayerScore_Add(self.owner, SP_KEEPAWAY_BCTIME, (autocvar_g_keepaway_score_timeinterval / 1)); // interval is divided by 1 so that time always shows "seconds"
124                 self.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(!self) { 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(self);
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, self.origin, '0 0 0', 1);
142                 sound(self, CH_TRIGGER, SND_KA_TOUCH, VOL_BASE, ATTEN_NORM);
143                 return;
144         }
145         else if(self.wait > time) { return; }
146
147         // attach the ball to the player
148         self.owner = other;
149         other.ballcarried = self;
150         setattachment(self, other, "");
151         setorigin(self, '0 0 0');
152
153         // make the ball invisible/unable to do anything/set up time scoring
154         self.velocity = '0 0 0';
155         self.movetype = MOVETYPE_NONE;
156         self.effects |= EF_NODRAW;
157         settouch(self, func_null);
158         setthink(self, ka_TimeScoring);
159         self.nextthink = time + autocvar_g_keepaway_score_timeinterval;
160         self.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, world, 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(self.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(self.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, world, "");
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 = world;
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, world, MSG_INFO, INFO_KEEPAWAY_DROPPED, plyr.netname);
213         Send_Notification(NOTIF_ALL, world, 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', world, 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         if((frag_attacker != frag_target) && (IS_PLAYER(frag_attacker)))
320         {
321                 if(frag_target.ballcarried) { // add to amount of times killing carrier
322                         PlayerScore_Add(frag_attacker, SP_KEEPAWAY_CARRIERKILLS, 1);
323                         if(autocvar_g_keepaway_score_bckill) // add bckills to the score
324                                 PlayerScore_Add(frag_attacker, SP_SCORE, autocvar_g_keepaway_score_bckill);
325                 }
326                 else if(!frag_attacker.ballcarried)
327                         if(autocvar_g_keepaway_noncarrier_warn)
328                                 Send_Notification(NOTIF_ONE_ONLY, frag_attacker, MSG_CENTER, CENTER_KEEPAWAY_WARN);
329
330                 if(frag_attacker.ballcarried) // add to amount of kills while ballcarrier
331                         PlayerScore_Add(frag_attacker, SP_SCORE, autocvar_g_keepaway_score_killac);
332         }
333
334         if(frag_target.ballcarried) { ka_DropEvent(frag_target); } // a player with the ball has died, drop it
335         return 0;
336 }
337
338 MUTATOR_HOOKFUNCTION(ka, GiveFragsForKill)
339 {
340         frag_score = 0; // no frags counted in keepaway
341         return 1; // you deceptive little bugger ;3 This needs to be true in order for this function to even count.
342 }
343
344 MUTATOR_HOOKFUNCTION(ka, PlayerPreThink)
345 {SELFPARAM();
346         // clear the item used for the ball in keepaway
347         self.items &= ~IT_KEY1;
348
349         // if the player has the ball, make sure they have the item for it (Used for HUD primarily)
350         if(self.ballcarried)
351                 self.items |= IT_KEY1;
352
353         return 0;
354 }
355
356 MUTATOR_HOOKFUNCTION(ka, PlayerUseKey)
357 {SELFPARAM();
358         if(MUTATOR_RETURNVALUE == 0)
359         if(self.ballcarried)
360         {
361                 ka_DropEvent(self);
362                 return 1;
363         }
364         return 0;
365 }
366
367 MUTATOR_HOOKFUNCTION(ka, PlayerDamage_Calculate) // for changing damage and force values that are applied to players in g_damage.qc
368 {
369         if(frag_attacker.ballcarried) // if the attacker is a ballcarrier
370         {
371                 if(frag_target == frag_attacker) // damage done to yourself
372                 {
373                         frag_damage *= autocvar_g_keepaway_ballcarrier_selfdamage;
374                         frag_force *= autocvar_g_keepaway_ballcarrier_selfforce;
375                 }
376                 else // damage done to noncarriers
377                 {
378                         frag_damage *= autocvar_g_keepaway_ballcarrier_damage;
379                         frag_force *= autocvar_g_keepaway_ballcarrier_force;
380                 }
381         }
382         else if (!frag_target.ballcarried) // if the target is a noncarrier
383         {
384                 if(frag_target == frag_attacker) // damage done to yourself
385                 {
386                         frag_damage *= autocvar_g_keepaway_noncarrier_selfdamage;
387                         frag_force *= autocvar_g_keepaway_noncarrier_selfforce;
388                 }
389                 else // damage done to other noncarriers
390                 {
391                         frag_damage *= autocvar_g_keepaway_noncarrier_damage;
392                         frag_force *= autocvar_g_keepaway_noncarrier_force;
393                 }
394         }
395         return 0;
396 }
397
398 MUTATOR_HOOKFUNCTION(ka, ClientDisconnect)
399 {SELFPARAM();
400         if(self.ballcarried) { ka_DropEvent(self); } // a player with the ball has left the match, drop it
401         return 0;
402 }
403
404 MUTATOR_HOOKFUNCTION(ka, MakePlayerObserver)
405 {SELFPARAM();
406         if(self.ballcarried) { ka_DropEvent(self); } // a player with the ball has left the match, drop it
407         return 0;
408 }
409
410 MUTATOR_HOOKFUNCTION(ka, PlayerPowerups)
411 {SELFPARAM();
412         // In the future this hook is supposed to allow me to do some extra stuff with waypointsprites and invisibility powerup
413         // So bare with me until I can fix a certain bug with ka_ballcarrier_waypointsprite_visible_for_player()
414
415         self.effects &= ~autocvar_g_keepaway_ballcarrier_effects;
416
417         if(self.ballcarried)
418                 self.effects |= autocvar_g_keepaway_ballcarrier_effects;
419
420         return 0;
421 }
422
423 .float stat_sv_airspeedlimit_nonqw;
424 .float stat_sv_maxspeed;
425
426 MUTATOR_HOOKFUNCTION(ka, PlayerPhysics)
427 {SELFPARAM();
428         if(self.ballcarried)
429         {
430                 self.stat_sv_airspeedlimit_nonqw *= autocvar_g_keepaway_ballcarrier_highspeed;
431                 self.stat_sv_maxspeed *= autocvar_g_keepaway_ballcarrier_highspeed;
432         }
433         return false;
434 }
435
436 MUTATOR_HOOKFUNCTION(ka, BotShouldAttack)
437 {SELFPARAM();
438         // if neither player has ball then don't attack unless the ball is on the ground
439         if(!checkentity.ballcarried && !self.ballcarried && ka_ball.owner)
440                 return true;
441         return false;
442 }
443
444 MUTATOR_HOOKFUNCTION(ka, HavocBot_ChooseRole)
445 {SELFPARAM();
446         if (self.ballcarried)
447                 self.havocbot_role = havocbot_role_ka_carrier;
448         else
449                 self.havocbot_role = havocbot_role_ka_collector;
450         return true;
451 }
452
453 MUTATOR_HOOKFUNCTION(ka, DropSpecialItems)
454 {
455         if(frag_target.ballcarried)
456                 ka_DropEvent(frag_target);
457
458         return false;
459 }
460
461
462 // ==============
463 // Initialization
464 // ==============
465
466 void ka_SpawnBall() // loads various values for the ball, runs only once at start of match
467 {
468         entity e = new(keepawayball);
469         e.model = "models/orbs/orbblue.md3";
470         precache_model(e.model);
471         _setmodel(e, e.model);
472         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
473         e.damageforcescale = autocvar_g_keepawayball_damageforcescale;
474         e.takedamage = DAMAGE_YES;
475         e.solid = SOLID_TRIGGER;
476         e.movetype = MOVETYPE_BOUNCE;
477         e.glow_color = autocvar_g_keepawayball_trail_color;
478         e.glow_trail = true;
479         e.flags = FL_ITEM;
480         e.pushable = true;
481         e.reset = ka_Reset;
482         settouch(e, ka_TouchEvent);
483         e.owner = world;
484         ka_ball = e;
485
486         InitializeEntity(e, ka_RespawnBall, INITPRIO_SETLOCATION); // is this the right priority? Neh, I have no idea.. Well-- it works! So.
487 }
488
489 void ka_ScoreRules()
490 {
491         ScoreRules_basics(0, SFL_SORT_PRIO_PRIMARY, 0, true); // SFL_SORT_PRIO_PRIMARY
492         ScoreInfo_SetLabel_PlayerScore(SP_KEEPAWAY_PICKUPS,                     "pickups",              0);
493         ScoreInfo_SetLabel_PlayerScore(SP_KEEPAWAY_CARRIERKILLS,        "bckills",              0);
494         ScoreInfo_SetLabel_PlayerScore(SP_KEEPAWAY_BCTIME,                      "bctime",               SFL_SORT_PRIO_SECONDARY);
495         ScoreRules_basics_end();
496 }
497
498 void ka_Initialize() // run at the start of a match, initiates game mode
499 {
500         ka_ScoreRules();
501         ka_SpawnBall();
502 }
503
504 #endif