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