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