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