]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/br/sv_br.qc
make bots more aware of battle royale, no longer attempt to attack while still droppi...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / br / sv_br.qc
1 // battle royale
2 // author: Juhu
3
4 #include "sv_br.qh"
5 #include <server/elimination.qh>
6 #include <common/resources/sv_resources.qh>
7 #include <common/mutators/base.qh>
8
9 #define BR_KILLS_INSTANTLY(pl, dt) \
10     (!IN_SQUAD((pl)) || (br_SquadFindLastAlive((pl).br_squad, true) == (pl)) || ((dt) == DEATH_HURTTRIGGER.m_id) \
11     || ((dt) == DEATH_KILL.m_id) || ((dt) == DEATH_TEAMCHANGE.m_id) || ((dt) == DEATH_AUTOTEAMCHANGE.m_id) \
12     || DEATH_ISWEAPON(dt, WEP_VAPORIZER))
13
14 float br_CalculatePlayerDropAngle(entity this);
15 bool br_PositionDropMember(entity this, entity leader, int position, float disconnect_range);
16 void br_LastPlayerForSquad_Notify(entity squad);
17 void br_RemovePlayer(entity player);
18 void br_Revive(entity player);
19 void br_Start();
20 bool br_CheckPlayers();
21 int br_WinningCondition();
22
23 entity dropship;
24
25 bool squads_colored = false;
26
27 const float br_drop_time_secs = 1;
28 const float drop_speed_vertical_max = 0.9;
29 const float drop_distance_disconnect = 32;
30 const float drop_speed_crash = 0.9;
31 float br_event_supply_time;
32 float br_event_vehicle_time;
33 bool br_started = false;
34 .bool br_ring_warned;
35 .float br_drop_time;
36 .float br_force_drop_distance;
37 .int br_drop_launch;
38 .int br_drop_detached;
39 .float br_drop_position;
40 .bool br_drop_instructions;
41 .float br_ring_damage_time;
42
43 .entity br_bleeding_inflictor;
44 .entity br_bleeding_attacker;
45 .int br_bleeding_deathtype;
46 ..entity br_bleeding_weaponentity;
47
48 // weapon set restoring for revive/drop
49 .WepSet br_wepset_old;
50 .Weapon br_weapon_prev[MAX_WEAPONSLOTS];
51 .float br_lastweapon_prev[MAX_WEAPONSLOTS];
52
53 float autocvar_g_br_revive_health = 0.25;
54 float autocvar_g_br_bleeding_health = 0.5;
55 float autocvar_g_br_bleeding_armor = 50;
56 float autocvar_g_br_drop_damage = 0.5;
57 float autocvar_g_br_drop_speed_max = 2.5;
58 float autocvar_g_br_drop_speed_min = 1.25;
59 float autocvar_g_br_drop_speed_vertical_min = 0.1;
60 bool autocvar_g_br_squad_colors = true;
61 float autocvar_g_br_drop_accel_dive = 50;
62 float autocvar_g_br_drop_accel_turn = 600;
63 bool autocvar_g_br_startweapons = false;
64 float autocvar_g_br_squad_waypoint_distance = 1500;
65
66 MUTATOR_HOOKFUNCTION(br, reset_map_global)
67 {
68     dropship_path_length = 0; // this should kill the dropship
69     dropship_path_direction = '0 0 0';
70
71     if(ring)
72         delete(ring);
73     ring = dropship = NULL;
74 }
75
76 MUTATOR_HOOKFUNCTION(br, reset_map_players)
77 {
78     FOREACH_CLIENT(true, {
79         GameRules_scoring_add(it, BR_RANK, -GameRules_scoring_add(it, BR_RANK, 0));
80         GameRules_scoring_add(it, BR_SQUAD, -GameRules_scoring_add(it, BR_SQUAD, 0));
81         GameRules_scoring_add(it, BR_REVIVALS, -GameRules_scoring_add(it, BR_REVIVALS, 0));
82
83         STAT(DROP, it) = DROP_LANDED;
84         STAT(BLEEDING, it) = false;
85
86         br_RemovePlayer(it);
87
88         it.br_wepset_old = start_weapons;
89         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
90         {
91             it.br_weapon_prev[slot] = WEP_Null;
92             it.br_lastweapon_prev[slot] = 0;
93         }
94     });
95
96     br_SquadUpdateInfo();
97     return true;
98 }
99
100 MUTATOR_HOOKFUNCTION(br, CheckRules_World)
101 {
102     if(!br_started && !warmup_stage && (time > game_starttime) && br_CheckPlayers())
103         br_Start();
104
105     M_ARGV(0, float) = br_WinningCondition();
106     return true;
107 }
108
109 MUTATOR_HOOKFUNCTION(br, GiveFragsForKill, CBC_ORDER_FIRST)
110 {
111     M_ARGV(2, float) = 0; // no frags counted in Battle Royale
112     return true;
113 }
114
115 MUTATOR_HOOKFUNCTION(br, ForbidPlayerScore_Clear)
116 {
117     // don't clear player score
118     return true;
119 }
120
121 MUTATOR_HOOKFUNCTION(br, GetPlayerStatus)
122 {
123     entity player = M_ARGV(0, entity);
124     return IN_SQUAD(player);
125 }
126
127 MUTATOR_HOOKFUNCTION(br, ClientConnect)
128 {
129     entity player = M_ARGV(0, entity);
130
131     STAT(SQUADCOLORS, player) = squads_colored;
132
133     if(ring)
134         ring_timelimit(ring);
135
136     br_SquadUpdateInfo();
137 }
138
139 MUTATOR_HOOKFUNCTION(br, ClientDisconnect)
140 {
141     entity player = M_ARGV(0, entity);
142
143     br_RemovePlayer(player);
144     br_SquadUpdateInfo();
145 }
146
147 MUTATOR_HOOKFUNCTION(br, PutClientInServer)
148 {
149     entity player = M_ARGV(0, entity);
150
151     if (!br_started)
152     {
153         if(!warmup_stage && (time > game_starttime) && br_CheckPlayers())
154             STAT(DROP, player) = DROP_TRANSPORT; // inhibits the spawn effect when the match is about to start
155         return false;
156     }
157
158     if (IN_SQUAD(player))
159         Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_JOIN_DEAD);
160     else
161         Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_JOIN_LATE);
162
163     TRANSMUTE(Observer, player);
164 }
165
166 MUTATOR_HOOKFUNCTION(br, MakePlayerObserver)
167 {
168     entity player = M_ARGV(0, entity);
169     bool is_forced = M_ARGV(1, bool);
170
171     if(is_forced && IN_SQUAD(player))
172     {
173         br_SquadMember_Remove(player);
174         br_SquadUpdateInfo();
175     }
176
177     if(IN_SQUAD(player))
178     {
179         player.frags = FRAGS_PLAYER_OUT_OF_GAME;
180         return true;
181     }
182
183     return false;
184 }
185
186 MUTATOR_HOOKFUNCTION(br, SpectateCopy)
187 {
188     entity spectatee = M_ARGV(0, entity);
189     entity client = M_ARGV(1, entity);
190
191     STAT(DROP, client) = STAT(DROP, spectatee);
192     STAT(BLEEDING, client) = STAT(BLEEDING, spectatee);
193 }
194
195 MUTATOR_HOOKFUNCTION(br, SpectateSet)
196 {
197     entity client = M_ARGV(0, entity);
198     entity target = M_ARGV(1, entity);
199
200     return (IN_SQUAD(client) && !client.br_squad.br_squad_dead && DIFF_SQUAD(client, target));
201 }
202
203 MUTATOR_HOOKFUNCTION(br, SpectateNext)
204 {
205     entity client = M_ARGV(0, entity);
206
207     if(!IS_REAL_CLIENT(client) || !IN_SQUAD(client) || client.br_squad.br_squad_dead)
208         return false;
209
210     entity new_target;
211
212     if(client.enemy && client.enemy.br_squad_next)
213         new_target = client.enemy.br_squad_next;
214     else
215         new_target = client.br_squad.br_squad_first;
216
217     while((new_target == client) || IS_DEAD(new_target) || !IS_PLAYER(new_target))
218     {
219         new_target = new_target.br_squad_next;
220         if(!new_target)
221             new_target = client.br_squad.br_squad_first;
222     }
223     M_ARGV(1, entity) = new_target;
224
225     return true;
226 }
227
228 MUTATOR_HOOKFUNCTION(br, SpectatePrev)
229 {
230     entity client = M_ARGV(0, entity);
231
232     if(!IS_REAL_CLIENT(client) || !IN_SQUAD(client) || client.br_squad.br_squad_dead)
233         return MUT_SPECPREV_CONTINUE;
234
235     entity new_target;
236
237     if(client.enemy && client.enemy.br_squad_prev)
238         new_target = client.enemy.br_squad_prev;
239     else
240         new_target = client.br_squad.br_squad_last;
241
242     while((new_target == client) || IS_DEAD(new_target) || !IS_PLAYER(new_target))
243     {
244         new_target = new_target.br_squad_prev;
245         if(!new_target)
246             new_target = client.br_squad.br_squad_last;
247     }
248     M_ARGV(1, entity) = new_target;
249
250     return MUT_SPECPREV_FOUND;
251 }
252
253 MUTATOR_HOOKFUNCTION(br, ForbidSpawn)
254 {
255     return br_started;
256 }
257
258 MUTATOR_HOOKFUNCTION(br, WantWeapon)
259 {
260     if(autocvar_g_br_startweapons)
261         return false;
262
263     M_ARGV(1, float) = 0;
264     return true;
265 }
266
267 MUTATOR_HOOKFUNCTION(br, SetStartItems)
268 {
269     start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS);
270
271     start_health       = warmup_start_health       = cvar("g_br_start_health");
272     start_armorvalue   = warmup_start_armorvalue   = cvar("g_br_start_armor");
273     start_ammo_shells  = warmup_start_ammo_shells  = cvar("g_br_start_ammo_shells");
274     start_ammo_nails   = warmup_start_ammo_nails   = cvar("g_br_start_ammo_nails");
275     start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_br_start_ammo_rockets");
276     start_ammo_cells   = warmup_start_ammo_cells   = cvar("g_br_start_ammo_cells");
277     start_ammo_plasma  = warmup_start_ammo_plasma  = cvar("g_br_start_ammo_plasma");
278     start_ammo_fuel    = warmup_start_ammo_fuel    = cvar("g_br_start_ammo_fuel");
279 }
280
281 // adjusted freezetag reviving code
282 #ifdef IN_REVIVING_RANGE
283     #undef IN_REVIVING_RANGE
284 #endif
285
286 #define IN_REVIVING_RANGE(player, it, revive_extra_size) \
287     (it != player && IS_PLAYER(it) && !IS_DEAD(it) && SAME_SQUAD(it, player) \
288     && boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax))
289
290 MUTATOR_HOOKFUNCTION(br, PlayerPreThink, CBC_ORDER_FIRST)
291 {
292     entity player = M_ARGV(0, entity);
293
294     if (game_stopped || !frametime || !IS_PLAYER(player))
295         return true;
296
297     if(ring)
298     {
299         const float ring_damage_interval = 0.75;
300         vector current_origin;
301         if(!player.vehicle)
302             current_origin = player.origin + player.view_ofs;
303         else
304             current_origin = player.vehicle.origin;
305         if(vlen(current_origin - ring.origin) > ring_calculate_current_radius(ring))
306         {
307             if(!player.br_ring_warned)
308             {
309                 player.br_ring_warned = true;
310                 player.br_ring_damage_time = time + ring_damage_interval;
311                 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_BR_RING_WARN);
312             }
313
314             // ring damage
315             if (player.br_ring_damage_time < time)
316             {
317                 if(player.vehicle) // if the player is controlling a vehicle
318                 {
319                     if(autocvar_g_br_ring_exitvehicle)
320                         vehicles_exit(player.vehicle, VHEF_RELEASE); // begone!
321                     else
322                         vehicles_damage(player.vehicle, ring, ring, 10 * ring.strength * ring_damage_interval, DEATH_RING.m_id, DMG_NOWEP, player.vehicle.origin, '0 0 0');
323                 }
324                 Damage(player, ring, ring, ring.strength * ring_damage_interval, DEATH_RING.m_id, DMG_NOWEP, player.origin, '0 0 0');
325                 player.br_ring_damage_time = time + ring_damage_interval;
326             }
327         }
328         else
329         {
330             player.br_ring_warned = false;
331         }
332     }
333
334     if((STAT(DROP, player) == DROP_FALLING) && (player.br_drop_detached != 2) && IN_SQUAD(player) && (player != player.br_squad.br_squad_drop_leader)){
335         // atck2 has to be released then pressed to detach
336         if(!(STAT(PRESSED_KEYS, player) & KEY_ATCK2)){
337             if(player.br_drop_detached == 0){
338                 player.br_drop_detached = 1;
339             }
340         }
341         else{
342             if(player.br_drop_detached == 1){
343                 player.br_drop_detached = 2;
344                 Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
345             }
346         }
347     }
348
349     if(STAT(DROP, player) == DROP_TRANSPORT){
350         if(time > (player.br_squad.br_drop_time + br_drop_time_secs))
351         {
352             if(!player.br_drop_instructions)
353             {
354                 player.br_drop_instructions = true;
355                 Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_DROPSHIP);
356             }
357
358             // jump has to be released then pressed to launch
359             if(!(STAT(PRESSED_KEYS, player) & KEY_JUMP)){
360                 if(player.br_drop_launch == 0){
361                     player.br_drop_launch = 1;
362                 }
363             }
364             else{
365                 if(player.br_drop_launch == 1){
366                     player.br_drop_launch = 2;
367                 }
368             }
369         }
370
371         if(!(IS_REAL_CLIENT(player) && (player.br_drop_launch == 2)) && (dropship_path_length > player.br_squad.br_force_drop_distance)){
372             player.velocity = dropship_path_direction * dropship_speed;
373         }
374         else{
375             if(!(IN_SQUAD(player) && player.br_squad.br_squad_drop_leader))
376             {
377                 player.effects &= ~EF_NODRAW;
378                 player.takedamage = DAMAGE_AIM;
379                 player.solid = SOLID_SLIDEBOX;
380                 if(!autocvar__notarget)
381                     player.flags &= ~FL_NOTARGET;
382                 Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
383                 STAT(DROP, player) = DROP_FALLING;
384                 player.br_drop_detached = 0;
385                 float mindropspeed = PHYS_MAXAIRSPEED(player) * max(autocvar_g_br_drop_speed_min, 0); // no maxspeed_mod available here
386                 float maxdropspeed_ratio = drop_speed_vertical_max; // moving straight down is glitchy
387                 float mindropspeed_ratio = bound(0, autocvar_g_br_drop_speed_vertical_min, drop_speed_vertical_max);
388                 float pitch_view = max(player.v_angle.x, 0);
389
390                 // pitch_view angle needs to be between 0 and 90 degrees
391                 if(pitch_view > 90)
392                     pitch_view = 180 - pitch_view;
393
394                 player.velocity.x = cos(player.angles.y * DEG2RAD);
395                 player.velocity.y = sin(player.angles.y * DEG2RAD);
396                 player.velocity.z = -tan(bound(asin(mindropspeed_ratio), pitch_view * DEG2RAD, asin(maxdropspeed_ratio)));
397
398                 player.velocity = normalize(player.velocity) * mindropspeed;
399
400                 player.angles.x = br_CalculatePlayerDropAngle(player) - 90;
401                 player.angles.y = vectoangles(vec2(player.velocity)).y + 180;
402                 player.angles.z = 180;
403
404                 if(IN_SQUAD(player) && ((IS_REAL_CLIENT(player) && (player.br_drop_launch == 2)) || br_SquadIsBotsOnly(player.br_squad)))
405                 {
406                     player.br_squad.br_squad_drop_leader = player;
407
408                     bool other_side = false;
409                     int drop_position = 1;
410
411                     if(random() < 0.5)
412                         drop_position *= -1;
413
414                     FOREACH_CLIENT_RANDOM(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (STAT(DROP, it) == DROP_TRANSPORT), {
415                         it.effects &= ~EF_NODRAW;
416                         it.takedamage = DAMAGE_AIM;
417                         it.solid = SOLID_SLIDEBOX;
418                         if(!autocvar__notarget)
419                             it.flags &= ~FL_NOTARGET;
420                         Kill_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CPID_BR_DROP);
421                         STAT(DROP, it) = DROP_FALLING;
422                         it.br_drop_detached = 0;
423                         Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_BR_DROP_DETACH);
424
425                         it.br_drop_position = drop_position;
426                         if(other_side)
427                             drop_position += copysign(1, drop_position);
428                         drop_position *= -1;
429                         other_side = !other_side;
430
431                         br_PositionDropMember(it, player, it.br_drop_position, -1);
432
433                         it.velocity = player.velocity;
434                         it.angles = player.angles;
435                     });
436                 }
437             }
438         }
439     }
440
441     // adjusted freezetag reviving code
442     entity revivers_last = NULL;
443     entity revivers_first = NULL;
444
445     bool player_is_reviving = false;
446     bool player_is_being_revived = false;
447     vector revive_extra_size = '1 1 1' * max(autocvar_g_br_revive_extra_size, 0);
448     FOREACH_CLIENT(IS_PLAYER(it), {
449         // check if player is reviving anyone
450         if (STAT(BLEEDING, it))
451         {
452             if (STAT(BLEEDING, player))
453                 continue;
454             if (!IN_REVIVING_RANGE(player, it, revive_extra_size))
455                 continue;
456             player_is_reviving = true;
457             break;
458         }
459
460         if (!STAT(BLEEDING, player))
461             continue; // both player and it are NOT bleeding
462         if (!IN_REVIVING_RANGE(player, it, revive_extra_size))
463             continue;
464
465         // found a squadmate that is reviving player
466         if (revivers_last)
467             revivers_last.chain = it;
468         revivers_last = it;
469         if (!revivers_first)
470             revivers_first = it;
471         player_is_being_revived = true;
472     });
473     if (revivers_last)
474         revivers_last.chain = NULL;
475
476     if (!player_is_being_revived) // no squadmate nearby
477     {
478         float clearspeed = max(autocvar_g_br_revive_clearspeed, 0);
479         if (STAT(BLEEDING, player))
480             STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) - frametime * clearspeed, 1);
481         else if (!player_is_reviving)
482             STAT(REVIVE_PROGRESS, player) = 0; // reviving nobody
483     }
484     else // OK, there is at least one squadmate reviving us
485     {
486         float spd = max(autocvar_g_br_revive_speed, 0);
487         STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * spd, 1);
488
489         if(STAT(REVIVE_PROGRESS, player) >= 1)
490         {
491             br_Revive(player);
492
493             // EVERY squad mate nearby gets a point (even if multiple!)
494             for(entity it = revivers_first; it; it = it.chain)
495             {
496                 GameRules_scoring_add(it, BR_REVIVALS, +1);
497             }
498
499             Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_BR_REVIVED, revivers_first.netname);
500             Send_Notification(NOTIF_ONE, revivers_first, MSG_CENTER, CENTER_BR_REVIVE, player.netname);
501             Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_REVIVED, player.netname, revivers_first.netname);
502             if(autocvar_sv_eventlog)
503             {
504                 string revivers = "";
505                 for(entity it = revivers_first; it; it = it.chain)
506                     revivers = strcat(revivers, ftos(it.playerid), ",");
507                 revivers = substring(revivers, 0, strlen(revivers) - 1);
508                 GameLogEcho(strcat(":br:revival:", ftos(player.playerid), ":", revivers));
509             }
510         }
511
512         for(entity it = revivers_first; it; it = it.chain)
513             STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player);
514     }
515
516     if (STAT(BLEEDING, player))
517     {
518         entity player_wp = player.waypointsprite_attached;
519         if (player_is_being_revived)
520         {
521             WaypointSprite_UpdateSprites(player_wp, WP_BRReviving, WP_Null, WP_Null);
522             WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_BR_REVIVING_COLOR);
523         }
524         else
525         {
526             WaypointSprite_UpdateSprites(player_wp, WP_BRBleeding, WP_Null, WP_Null);
527             WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_BR_BLEEDING_COLOR);
528         }
529
530         WaypointSprite_UpdateMaxHealth(player_wp, 1);
531         WaypointSprite_UpdateHealth(player_wp, STAT(REVIVE_PROGRESS, player));
532     }
533
534     return true;
535 }
536
537 #undef IN_REVIVING_RANGE
538
539 MUTATOR_HOOKFUNCTION(br, PM_Physics)
540 {
541     entity player = M_ARGV(0, entity);
542     float maxspeed_mod = M_ARGV(1, float);
543     float dt = M_ARGV(2, float); // tick rate
544
545     if(STAT(DROP, player) == DROP_TRANSPORT)
546         return true;
547
548     // set the drop stat to landed on the next frame if it was set on landing
549     if(STAT(DROP, player) == DROP_LANDING)
550         STAT(DROP, player) = DROP_LANDED;
551
552     // TODO: improve dropping physics
553     if(STAT(DROP, player) == DROP_FALLING){
554         float maxairspeed = PHYS_MAXAIRSPEED(player) * max(maxspeed_mod, 1);
555         float mindropspeed = maxairspeed * max(autocvar_g_br_drop_speed_min, 0);
556         float dropspeed = vlen(vec2(player.velocity) + eZ * min(player.velocity.z, 0));
557         if(player.velocity.z > 0)
558             dropspeed -= vlen(player.velocity) - dropspeed;
559         if(!IS_ONGROUND(player) && (player.waterlevel < WATERLEVEL_SWIMMING) && (dropspeed >= (mindropspeed * drop_speed_crash)) && ((tracebox(player.origin, player.mins, player.maxs, player.origin - '0 0 1', MOVE_NOMONSTERS, player), trace_fraction) >= 1)) // IS_ONGROUND doesn't work if jump is held (jump is theoretically blocked until landed)
560         {
561             ITEMS_STAT(player) |= IT_USING_JETPACK;
562             bool has_drop_leader = IN_SQUAD(player) && (player.br_drop_detached != 2) && (player.br_squad.br_squad_drop_leader && (STAT(DROP, player.br_squad.br_squad_drop_leader) == DROP_FALLING));
563             bool player_is_drop_leader = has_drop_leader && (player == player.br_squad.br_squad_drop_leader);
564             if(player_is_drop_leader || !has_drop_leader)
565             {
566                 float maxdropspeed = maxairspeed * max(autocvar_g_br_drop_speed_max, 0);
567                 float maxdropspeed_ratio = drop_speed_vertical_max; // moving straight down is glitchy
568                 float mindropspeed_ratio = bound(0, autocvar_g_br_drop_speed_vertical_min, drop_speed_vertical_max);
569                 float accel_dive = max(autocvar_g_br_drop_accel_dive, 0);
570                 float accel_turn = max(autocvar_g_br_drop_accel_turn, 0);
571                 float dropspeed_xy = vlen(vec2(player.velocity));
572                 float pitch_current = br_CalculatePlayerDropAngle(player);
573                 float pitch_view = max(player.v_angle.x, 0);
574
575                 // pitch_view angle needs to be between 0 and 90 degrees
576                 if(pitch_view > 90)
577                     pitch_view = 180 - pitch_view;
578
579                 float pitch_diff = pitch_current - pitch_view;
580                 float pitch_ratio_wish = 0;
581
582                 // calculate how much the player wants to change pitch (ratio is at least 0.1)
583                 // ratio is between -1 (looking straight down) and +1 (looking straight ahead or up)
584                 if((pitch_diff < 0) && (pitch_current < 90))
585                     pitch_ratio_wish = bound(-1, sin(pitch_diff / (90 - pitch_current) * M_PI_2), -0.1);
586                 else if((pitch_diff > 0) && (pitch_current > 0))
587                     pitch_ratio_wish = bound(0.1, sin(pitch_diff / pitch_current * M_PI_2), 1);
588
589                 makevectors(player.v_angle);
590                 // horizontal wishvel as usual
591                 vector wishvel = v_forward * CS(player).movement.x + v_right * CS(player).movement.y;
592
593                 // except make turning backwards easier by limiting the maximum turning angle to 90 degrees
594                 vector wish_angles = vectoangles(vec2(wishvel));
595                 vector vel_angles = vectoangles(vec2(player.velocity));
596
597                 float diff_angle = wish_angles.y - vel_angles.y;
598                 if(diff_angle > 180)
599                     diff_angle -= 360;
600                 if(diff_angle < -180)
601                     diff_angle += 360;
602
603                 wish_angles.y = (vel_angles.y + bound(-90, diff_angle, 90) + 360) % 360;
604                 makevectors(wish_angles);
605
606                 wishvel = normalize(v_forward) * min(1, vlen(wishvel) / maxairspeed);
607                 // vertical wishvel using forward movement and the previously calculated ratio
608                 wishvel.z = pitch_ratio_wish * bound(0, CS(player).movement.x / maxairspeed, 1);
609                 // apply turn acceleration to wishvel
610                 wishvel *= accel_turn;
611                 player.velocity += wishvel * dt;
612                 player.velocity = normalize(eZ * player.velocity.z + normalize(vec2(player.velocity)) * dropspeed_xy);
613
614                 // if there is no horizontal movement point the horizontal vector towards the view direction
615                 if(vlen(vec2(player.velocity)) == 0)
616                     player.velocity += (eX * cos(player.angles.y * DEG2RAD) + eY * sin(player.angles.y * DEG2RAD)) * sqrt(1 - pow(maxdropspeed_ratio, 2));
617
618                 // modify mindropspeed_ratio and maxdropspeed_ratio so that the player does not rotate beyond the view angle
619                 float pitch_ratio_view = sin(pitch_view * DEG2RAD);
620                 if(pitch_ratio_wish > 0)
621                     mindropspeed_ratio = bound(mindropspeed_ratio, pitch_ratio_view, maxdropspeed_ratio);
622                 else if(pitch_ratio_wish < 0)
623                     maxdropspeed_ratio = bound(mindropspeed_ratio, pitch_ratio_view, maxdropspeed_ratio);
624
625                 // constrain to vertical min/maxdropspeed
626                 if(player.velocity.z > -mindropspeed_ratio)
627                     player.velocity.z = -mindropspeed_ratio;
628                 if(player.velocity.z < -maxdropspeed_ratio)
629                     player.velocity.z = -maxdropspeed_ratio;
630
631                 // adjust horizontal speed so that vertical speed + horizontal speed = maxdropspeed
632                 float dropangle = br_CalculatePlayerDropAngle(player);
633                 const float accelangle = 20;
634                 dropspeed = bound(mindropspeed, dropspeed + accel_dive * (dropangle - accelangle) / accelangle * dt, maxdropspeed);
635                 player.velocity = normalize(player.velocity) * dropspeed;
636
637                 player.angles.x = dropangle - 90;
638                 player.angles.y = vectoangles(vec2(player.velocity)).y + 180;
639                 player.angles.z = 180;
640
641                 if(player_is_drop_leader)
642                 {
643                     FOREACH_CLIENT(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (it.br_drop_detached != 2) && (STAT(DROP, it) == DROP_FALLING), {
644                         if(!br_PositionDropMember(it, player, it.br_drop_position, drop_distance_disconnect))
645                         {
646                             it.br_drop_detached = 2;
647                             Kill_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CPID_BR_DROP);
648                             continue;
649                         }
650
651                         it.velocity = player.velocity;
652                         it.angles = player.angles;
653                     });
654                 }
655                 else if((player.br_drop_detached != 2) && IN_SQUAD(player))
656                 {
657                     player.br_drop_detached = 2;
658                     Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
659                 }
660             }
661             else
662             {
663                 if(!br_PositionDropMember(player, player.br_squad.br_squad_drop_leader, player.br_drop_position, drop_distance_disconnect))
664                 {
665                     player.br_drop_detached = 2;
666                     Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
667                 }
668
669                 player.velocity = player.br_squad.br_squad_drop_leader.velocity;
670                 player.angles = player.br_squad.br_squad_drop_leader.angles; // no fixangles, only moves the player model not the player view
671             }
672
673             return true;
674         }
675         else
676         {
677             if((player.br_drop_detached != 2) && IN_SQUAD(player) && (player != player.br_squad.br_squad_drop_leader))
678             {
679                 player.br_drop_detached = 2;
680                 Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP);
681             }
682
683             STAT(DROP, player) = DROP_LANDING;
684             set_movetype(player, MOVETYPE_WALK);
685             ITEMS_STAT(player) &= ~IT_USING_JETPACK;
686             player.flags |= FL_PICKUPITEMS;
687             player.dphitcontentsmask |= DPCONTENTS_BODY;
688
689             STAT(WEAPONS, player) = player.br_wepset_old;
690
691             .entity weaponentity = weaponentities[0];
692             W_SwitchWeapon_Force(player, w_getbestweapon(player, weaponentity), weaponentity);
693         }
694     }
695
696     // injured players can't swim
697     if(STAT(BLEEDING, player)){
698         if(player.waterlevel >= WATERLEVEL_SWIMMING)
699         {
700             CS(player).movement.z = -60; // drift towards bottom
701             player.v_angle.x = 0;
702             player.com_in_jump = false;
703         }
704     }
705 }
706
707 MUTATOR_HOOKFUNCTION(br, Damage_Calculate)
708 {
709     entity frag_target = M_ARGV(2, entity);
710     float frag_deathtype = M_ARGV(3, float);
711
712     if(STAT(DROP, frag_target) != DROP_LANDED)
713     {
714         // weapon impact has no push force while dropping
715         M_ARGV(6, vector) = '0 0 0';
716
717         if(STAT(DROP, frag_target) == DROP_TRANSPORT)
718             M_ARGV(4, float) = M_ARGV(5, float) = 0; // can't take damage while on the dropship
719         else if(DEATH_ISWEAPON(frag_deathtype, WEP_VAPORIZER)); // do not adjust vaporizer damage
720         else
721         {
722             switch(frag_deathtype)
723             {
724                 case DEATH_FALL.m_id:
725                 case DEATH_SHOOTING_STAR.m_id:
726                     // do not take fall damage when landing from dropship
727                     M_ARGV(4, float) = M_ARGV(5, float) = 0;
728                     break;
729                 default:
730                     // only take half of the usual damage
731                     M_ARGV(4, float) *= max(autocvar_g_br_drop_damage, 0);
732                     M_ARGV(5, float) *= max(autocvar_g_br_drop_damage, 0);
733             }
734         }
735     }
736 }
737
738 MUTATOR_HOOKFUNCTION(br, PlayerDies, CBC_ORDER_FIRST)
739 {
740     entity frag_attacker = M_ARGV(1, entity);
741     entity frag_target = M_ARGV(2, entity);
742     float frag_deathtype = M_ARGV(3, float); // float for some reason, breaks if changed to int
743
744     if(!IS_PLAYER(frag_target))
745         return true;
746
747     if(STAT(DROP, frag_target) == DROP_TRANSPORT)
748     {
749         frag_target.effects &= ~EF_NODRAW;
750         frag_target.takedamage = DAMAGE_AIM;
751         frag_target.solid = SOLID_SLIDEBOX;
752         if(!autocvar__notarget)
753             frag_target.flags &= ~FL_NOTARGET;
754         Kill_Notification(NOTIF_ONE_ONLY, frag_target, MSG_CENTER, CPID_BR_DROP);
755     }
756
757     if(STAT(DROP, frag_target) == DROP_FALLING)
758     {
759         if((frag_target.br_drop_detached != 2) && IN_SQUAD(frag_target) && (frag_target != frag_target.br_squad.br_squad_drop_leader))
760         {
761             frag_target.br_drop_detached = 2;
762             Kill_Notification(NOTIF_ONE_ONLY, frag_target, MSG_CENTER, CPID_BR_DROP);
763         }
764     }
765
766     if(STAT(DROP, frag_target) != DROP_LANDED)
767     {
768         set_movetype(frag_target, MOVETYPE_WALK);
769         frag_target.dphitcontentsmask |= DPCONTENTS_BODY;
770         STAT(WEAPONS, frag_target) = frag_target.br_wepset_old;
771         STAT(DROP, frag_target) = DROP_LANDED;
772     }
773
774     if(STAT(BLEEDING, frag_target) || BR_KILLS_INSTANTLY(frag_target, frag_deathtype))
775     {
776         if(STAT(BLEEDING, frag_target))
777         {
778             Kill_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CPID_BR_DOWN);
779             STAT(BLEEDING, frag_target) = false;
780
781             // restore weapons on death to make weapon drop work
782             STAT(WEAPONS, frag_target) = frag_target.br_wepset_old;
783             for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
784             {
785                 .entity weaponentity = weaponentities[slot];
786                 frag_target.(weaponentity).m_weapon = frag_target.br_weapon_prev[slot];
787             }
788         }
789         WaypointSprite_Kill(frag_target.br_allywaypoint);
790
791         frag_target.respawn_flags = RESPAWN_SILENT | RESPAWN_FORCE;
792         frag_target.respawn_time = time + 2;
793         return true;
794     }
795
796     frag_target.flags &= ~FL_PICKUPITEMS;
797     RemoveGrapplingHooks(frag_target);
798     StatusEffects_removeall(frag_target, STATUSEFFECT_REMOVE_NORMAL);
799
800     SetResource(frag_target, RES_HEALTH, start_health * max(autocvar_g_br_bleeding_health, 0));
801     SetResource(frag_target, RES_ARMOR, max(autocvar_g_br_bleeding_armor, 0));
802     Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN_WAIT);
803     STAT(BLEEDING, frag_target) = true;
804
805     FOREACH_CLIENT(IS_PLAYER(it),
806     {
807         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
808         {
809             .entity weaponentity = weaponentities[slot];
810             if(it.(weaponentity).hook.aiment == frag_target)
811                 RemoveHook(it.(weaponentity).hook);
812         }
813     });
814
815     frag_target.br_wepset_old = STAT(WEAPONS, frag_target);
816     for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
817     {
818         .entity weaponentity = weaponentities[slot];
819         frag_target.br_weapon_prev[slot] = frag_target.(weaponentity).m_switchweapon;
820         frag_target.br_lastweapon_prev[slot] = frag_target.(weaponentity).cnt;
821     }
822     STAT(WEAPONS, frag_target) = '0 0 0';
823
824     WaypointSprite_Spawn(WP_BRBleeding, 0, 0, frag_target, '0 0 64', NULL, 0, frag_target, waypointsprite_attached, true, RADARICON_WAYPOINT);
825
826     if(frag_attacker == frag_target || !frag_attacker || ITEM_DAMAGE_NEEDKILL(frag_deathtype))
827     {
828         if(IS_PLAYER(frag_target))
829             Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN_SELF);
830         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_DOWN_SELF, frag_target.netname);
831     }
832     else
833     {
834         if(IS_PLAYER(frag_target))
835             Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN, frag_attacker.netname);
836         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_DOWN, frag_target.netname, frag_attacker.netname);
837     }
838
839     br_SquadUpdateInfo();
840
841     return true;
842 }
843
844 MUTATOR_HOOKFUNCTION(br, PlayerDied)
845 {
846     entity player = M_ARGV(0, entity);
847     if(br_started)
848     {
849         br_LastPlayerForSquad_Notify(player.br_squad);
850         br_SquadUpdateInfo();
851     }
852 }
853
854 MUTATOR_HOOKFUNCTION(br, ClientObituary)
855 {
856     entity frag_inflictor = M_ARGV(0, entity);
857     entity frag_attacker = M_ARGV(1, entity);
858     entity frag_target = M_ARGV(2, entity);
859     float frag_deathtype = M_ARGV(3, float); // float for some reason, breaks if changed to int
860     //entity frag_weaponentity = M_ARGV(4, entity);
861
862     if(!STAT(BLEEDING, frag_target) && !BR_KILLS_INSTANTLY(frag_target, frag_deathtype))
863     {
864         frag_target.br_bleeding_inflictor = frag_inflictor;
865         frag_target.br_bleeding_attacker = frag_attacker;
866         frag_target.br_bleeding_deathtype = frag_deathtype;
867         //frag_target.br_bleeding_weaponentity = frag_weaponentity; // TODO: get entity field
868         return true;
869     }
870
871     if(STAT(BLEEDING, frag_target) && frag_target.br_bleeding_attacker)
872     {
873         entity new_inflictor = frag_target.br_bleeding_inflictor;
874         entity new_attacker = frag_target.br_bleeding_attacker;
875         int new_deathtype = frag_target.br_bleeding_deathtype;
876         .entity new_weaponentity = frag_target.br_bleeding_weaponentity;
877         frag_target.br_bleeding_attacker = frag_target.br_bleeding_inflictor = NULL;
878
879         Obituary(new_attacker, new_inflictor, frag_target, new_deathtype, new_weaponentity);
880         return true;
881     }
882
883     return false;
884 }
885
886 MUTATOR_HOOKFUNCTION(br, SetResource)
887 {
888     entity player = M_ARGV(7, entity);
889     if(!IS_PLAYER(player))
890         return false;
891
892     entity res_type = M_ARGV(8, entity);
893     float amount = M_ARGV(9, float);
894
895     if(STAT(BLEEDING, player) && (res_type == RES_HEALTH || res_type == RES_ARMOR))
896     {
897         if(amount > GetResource(player, res_type)) // prevent the player from getting health or armor in any way
898             return true;
899     }
900 }
901
902 MUTATOR_HOOKFUNCTION(br, GetResourceLimit)
903 {
904     entity player = M_ARGV(7, entity);
905
906     if(!IS_PLAYER(player) || !STAT(BLEEDING, player))
907         return false;
908
909     entity res_type = M_ARGV(8, entity);
910
911     switch(res_type)
912     {
913         case RES_HEALTH:
914             M_ARGV(9, float) *= max(autocvar_g_br_bleeding_health, 0);
915             break;
916         case RES_ARMOR:
917             M_ARGV(9, float) = max(autocvar_g_br_bleeding_armor, 0);
918     }
919 }
920
921 MUTATOR_HOOKFUNCTION(br, PlayerRegen, CBC_ORDER_FIRST)
922 {
923     entity player = M_ARGV(0, entity);
924
925     if(STAT(BLEEDING, player)){
926         M_ARGV(7, float) = max(autocvar_g_br_bleed, 0);
927         M_ARGV(8, float) = max(autocvar_g_br_bleedlinear, 0);
928         M_ARGV(2, float) = M_ARGV(10, float) = 0;
929     }
930     else{
931         M_ARGV(2, float) = M_ARGV(3, float) = 0; // no regeneration or rot in battle royale
932     }
933 }
934
935 MUTATOR_HOOKFUNCTION(br, PlayerCanCrouch)
936 {
937     entity player = M_ARGV(0, entity);
938     if(STAT(BLEEDING, player))
939         M_ARGV(1, bool) = true;
940     else if(STAT(DROP, player) != DROP_LANDED)
941         M_ARGV(1, bool) = false;
942 }
943
944 MUTATOR_HOOKFUNCTION(br, PlayerJump)
945 {
946     entity player = M_ARGV(0, entity);
947     return STAT(BLEEDING, player) || (STAT(DROP, player) != DROP_LANDED);
948 }
949
950 MUTATOR_HOOKFUNCTION(br, BotShouldAttack)
951 {
952     entity bot = M_ARGV(0, entity);
953     entity target = M_ARGV(1, entity);
954
955     return SAME_SQUAD(bot, target) || (STAT(DROP, bot) != DROP_LANDED) || (STAT(DROP, target) == DROP_TRANSPORT);
956 }
957
958 MUTATOR_HOOKFUNCTION(br, TurretValidateTarget)
959 {
960     entity turret = M_ARGV(0, entity);
961     entity target = M_ARGV(1, entity);
962
963     if(!br_started || SAME_SQUAD(turret, target) || (STAT(DROP, target) == DROP_TRANSPORT))
964     {
965         M_ARGV(3, float) = -1;
966         return true;
967     }
968
969     return false;
970 }
971
972 MUTATOR_HOOKFUNCTION(br, AccuracyTargetValid)
973 {
974     entity attacker = M_ARGV(0, entity);
975     entity target = M_ARGV(1, entity);
976
977     if(SAME_SQUAD(attacker, target) || (STAT(DROP, target) == DROP_TRANSPORT))
978         return MUT_ACCADD_INDIFFERENT;
979     return MUT_ACCADD_VALID;
980 }
981
982 MUTATOR_HOOKFUNCTION(br, CustomizeWaypoint)
983 {
984     entity wp = M_ARGV(0, entity);
985     entity player = M_ARGV(1, entity);
986
987     if(wp.owner == NULL)
988         return true;
989
990     if((wp == wp.owner.br_allywaypoint) && (vdist(wp.owner.origin - player.origin, <, autocvar_g_br_squad_waypoint_distance) || STAT(BLEEDING, wp.owner)))
991         return true;
992
993     if(!IS_PLAYER(player) || (IN_SQUAD(wp.owner) && DIFF_SQUAD(wp.owner, player)))
994         return true;
995 }
996
997 MUTATOR_HOOKFUNCTION(br, ClientKill)
998 {
999     entity player = M_ARGV(0, entity);
1000
1001     if(br_started)
1002     {
1003         // no forfeiting once the game started
1004         Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_FORFEIT);
1005         return true;
1006     }
1007
1008     return false;
1009 }
1010
1011 MUTATOR_HOOKFUNCTION(br, ClientCommand_Spectate)
1012 {
1013     entity player = M_ARGV(0, entity);
1014
1015     if(br_started)
1016     {
1017         // no forfeiting once the game started
1018         Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_FORFEIT);
1019         return MUT_SPECCMD_RETURN;
1020     }
1021     return MUT_SPECCMD_CONTINUE;
1022 }
1023
1024 MUTATOR_HOOKFUNCTION(br, PlayerSpawn)
1025 {
1026     if(!br_started && !warmup_stage && (time > game_starttime) && br_CheckPlayers())
1027         br_Start();
1028 }
1029
1030 MUTATOR_HOOKFUNCTION(br, SV_StartFrame)
1031 {
1032     if(!br_started)
1033         return false;
1034
1035     if(autocvar_g_br_supply_interval > 0 && time - br_event_supply_time >= autocvar_g_br_supply_interval)
1036     {
1037         br_event_supply_time = time;
1038         spawn_supply();
1039     }
1040
1041     if(autocvar_g_br_vehicle_interval > 0 && time - br_event_vehicle_time >= autocvar_g_br_vehicle_interval)
1042     {
1043         br_event_vehicle_time = time;
1044         spawn_vehicle();
1045     }
1046
1047     if(ring)
1048     {
1049         float current_radius = ring_calculate_current_radius(ring);
1050
1051         IL_EACH(g_items, it.bot_pickup, {
1052             if(vdist(it.origin - ring.origin, >, current_radius))
1053                 it.bot_pickup = false;
1054         });
1055     }
1056 }
1057
1058 void(entity this) havocbot_role_br_reviving;
1059 void(entity this) havocbot_role_br_generic;
1060
1061 bool squad_needs_revive(entity this)
1062 {
1063     for(entity member = this.br_squad.br_squad_first; member; member = member.br_squad_next)
1064     {
1065         if(IS_DEAD(member) || !IS_PLAYER(member))
1066             continue;
1067
1068         if(STAT(BLEEDING, member))
1069             return true;
1070     }
1071
1072     return false;
1073 }
1074
1075 bool br_bot_ignore_in_ring(entity this)
1076 {
1077     if(!ring)
1078         return false;
1079
1080     if(vlen(this.origin - ring.origin) > ring_calculate_current_radius(ring))
1081         return true;
1082
1083     return false;
1084 }
1085
1086 void havocbot_goalrating_br_findplayers(entity this, float ratingscale)
1087 {
1088     if(!IN_SQUAD(this))
1089         return;
1090
1091     for(entity member = this.br_squad.br_squad_first; member; member = member.br_squad_next)
1092     {
1093         if(IS_DEAD(member) || !IS_PLAYER(member) || (member == this))
1094             continue;
1095
1096         // either wants to be revived by another player or wants to revive another player
1097         if(STAT(BLEEDING, member) != STAT(BLEEDING, this))
1098             navigation_routerating(this, member, ratingscale, 100000);
1099     }
1100 }
1101
1102 void havocbot_role_br_reviving(entity this)
1103 {
1104     if(IS_DEAD(this))
1105         return;
1106
1107     if(!squad_needs_revive(this))
1108     {
1109         LOG_TRACE("changing role to generic");
1110         this.havocbot_role = havocbot_role_br_generic;
1111         navigation_goalrating_timeout_force(this);
1112         return;
1113     }
1114
1115     navigation_goalrating_start(this);
1116     havocbot_goalrating_br_findplayers(this, 20000);
1117     navigation_goalrating_end(this);
1118 }
1119
1120 void havocbot_role_br_generic(entity this)
1121 {
1122     if(IS_DEAD(this))
1123         return;
1124
1125     if(squad_needs_revive(this))
1126     {
1127         LOG_TRACE("changing role to reviving");
1128         this.havocbot_role = havocbot_role_br_reviving;
1129         return;
1130     }
1131
1132     havocbot_role_generic(this);
1133 }
1134
1135 MUTATOR_HOOKFUNCTION(br, HavocBot_ChooseRole)
1136 {
1137     entity bot = M_ARGV(0, entity);
1138
1139     if(!IS_DEAD(bot))
1140         bot.havocbot_role = havocbot_role_br_generic;
1141
1142     return true;
1143 }
1144
1145 float br_CalculatePlayerDropAngle(entity this)
1146 {
1147     if(this.velocity.z < 0)
1148     {
1149         float dropspeed_xy = vlen(vec2(this.velocity));
1150         float dropspeed_z = fabs(this.velocity.z);
1151         return 90 - atan(dropspeed_xy / dropspeed_z) * RAD2DEG;
1152     }
1153
1154     return 0;
1155 }
1156
1157 bool br_PositionDropMember(entity this, entity leader, int position, float disconnect_range) {
1158     float pl_mins = min(leader.mins.x, leader.mins.y);
1159     float pl_maxs = max(leader.maxs.x, leader.maxs.y);
1160
1161     vector member_offset;
1162     member_offset.x = cos((leader.angles.y + 90) * DEG2RAD);
1163     member_offset.y = sin((leader.angles.y + 90) * DEG2RAD);
1164     member_offset.z = 0;
1165     member_offset *= pl_maxs - pl_mins + 32; // I hope individual players never get different mins/maxs
1166     member_offset *= position;
1167
1168     vector member_destination = leader.origin + member_offset;
1169
1170     tracebox(this.origin, this.mins, this.maxs, member_destination, MOVE_NORMAL, this);
1171
1172     if((trace_fraction < 1) && (disconnect_range >= 0))
1173     {
1174         if(vlen(member_destination - trace_endpos) > disconnect_range)
1175             return false;
1176     }
1177
1178     setorigin(this, trace_endpos);
1179     return true;
1180 }
1181
1182 void br_LastPlayerForSquad_Notify(entity squad)
1183 {
1184     entity player = br_SquadFindLastAlive(squad, false);
1185     if(player)
1186         Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ALONE);
1187 }
1188
1189 void br_RemovePlayer(entity player)
1190 {
1191     br_SquadMember_Remove(player);
1192
1193     FOREACH_CLIENT((it.br_bleeding_attacker == player) || (it.br_bleeding_inflictor == player), {
1194         it.br_bleeding_attacker = it.br_bleeding_inflictor = NULL;
1195     });
1196 }
1197
1198 void br_Revive(entity player)
1199 {
1200     if(STAT(BLEEDING, player))
1201     {
1202         Kill_Notification(NOTIF_ONE, player, MSG_CENTER, CPID_BR_DOWN);
1203         STAT(BLEEDING, player) = false;
1204     }
1205     player.flags |= FL_PICKUPITEMS;
1206     SetResource(player, RES_HEALTH, start_health * max(autocvar_g_br_revive_health, 0));
1207     SetResource(player, RES_ARMOR, 0);
1208
1209     STAT(WEAPONS, player) = player.br_wepset_old;
1210     for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1211     {
1212         .entity weaponentity = weaponentities[slot];
1213         W_SwitchWeapon_Force(player, player.br_weapon_prev[slot], weaponentity);
1214         player.(weaponentity).cnt = player.br_lastweapon_prev[slot];
1215     }
1216
1217     player.pauseregen_finished = time + autocvar_g_balance_pause_health_regen;
1218
1219     STAT(REVIVE_PROGRESS, player) = 0;
1220     player.revival_time = time;
1221
1222     WaypointSprite_Kill(player.waypointsprite_attached);
1223
1224     br_SquadUpdateInfo();
1225 }
1226
1227 int br_WinningCondition()
1228 {
1229     int total_squads = br_SquadUpdateInfo();
1230
1231     if ((total_squads > 1) || !br_started)
1232         return WINNING_NEVER;
1233
1234     entity winner_squad = NULL;
1235     IL_EACH(squads, !it.br_squad_dead, { winner_squad = it; break; });
1236
1237     for(entity member = winner_squad.br_squad_first; member; member = member.br_squad_next)
1238     {
1239         GameRules_scoring_add(member, BR_RANK, 1);
1240         member.winning = true;
1241     }
1242
1243     delete(round_handler);
1244     round_handler = NULL;
1245
1246     return WINNING_YES;
1247 }
1248
1249 bool br_isEliminated(entity e)
1250 {
1251     return (IN_SQUAD(e) && (IS_DEAD(e) || !IS_PLAYER(e)));
1252 }
1253
1254 bool br_CheckPlayers()
1255 {
1256     total_players = 0;
1257     FOREACH_CLIENT(IS_PLAYER(it), ++total_players);
1258
1259     static int prev_players = 0;
1260     if (total_players >= autocvar_g_br_minplayers || total_players == 0)
1261     {
1262         if(prev_players > 0)
1263             Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS);
1264         prev_players = 0;
1265         return (total_players >= autocvar_g_br_minplayers);
1266     }
1267
1268     if(prev_players != total_players)
1269     {
1270         Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, autocvar_g_br_minplayers - total_players);
1271         prev_players = total_players;
1272     }
1273
1274     return false;
1275 }
1276
1277 void br_Start(){
1278     // battle royale does not need those, besides, the timelimit won't be visible anymore after the game started
1279     cvar_set("timelimit", "0");
1280     cvar_set("fraglimit", "0");
1281     cvar_set("leadlimit", "0");
1282
1283     reset_map(true, false);
1284
1285     ring = ring_initialize();
1286     dropship = dropship_initialize();
1287
1288     if(!ring || !dropship)
1289     {
1290         if(!ring)
1291             LOG_WARN("Failed to determine ring starting point");
1292         if(!dropship)
1293             LOG_WARN("Failed to determine dropship route");
1294
1295         LOG_WARN("Prerequisites not met. Cannot start battle royale, aborting...");
1296
1297         if(ring)
1298         {
1299             delete(ring);
1300             ring = NULL;
1301         }
1302
1303         if(dropship)
1304         {
1305             delete(dropship);
1306             dropship = NULL;
1307         }
1308
1309         NextLevel();
1310         return;
1311     }
1312
1313     br_started = true;
1314     int num_players = 0;
1315
1316     FOREACH_CLIENT(IS_PLAYER(it), {
1317         STAT(DROP, it) = DROP_TRANSPORT;
1318         PutPlayerInServer(it);
1319
1320         it.br_wepset_old = STAT(WEAPONS, it);
1321         STAT(WEAPONS, it) = '0 0 0';
1322         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
1323         {
1324             it.br_weapon_prev[slot] = WEP_Null;
1325             it.br_lastweapon_prev[slot] = 0;
1326         }
1327
1328         ++num_players;
1329     });
1330
1331     max_squad_size = max(autocvar_g_br_squad_size, 1);
1332     if(num_players <= max_squad_size)
1333         max_squad_size = ceil(num_players / 2);
1334
1335     for(int num_squads = 0; (num_squads * max_squad_size) < num_players; ++num_squads)
1336     {
1337         entity new_squad = new_pure(squad);
1338         new_squad.br_squad_drop_leader = NULL;
1339         new_squad.br_squad_id = num_squads + 1;
1340
1341         IL_PUSH(squads, new_squad);
1342     }
1343
1344     FOREACH_CLIENT_RANDOM(IS_PLAYER(it), {
1345         entity current_squad = br_SquadGetRandomAvail();
1346         br_SquadMember_Add(current_squad, it);
1347         GameRules_scoring_add(it, BR_SQUAD, current_squad.br_squad_id);
1348
1349         setorigin(it, dropship.origin + eZ * (dropship.mins.z - it.maxs.z - 64));
1350         it.angles = vectoangles(dropship_path_direction) + '45 0 0';
1351         it.fixangle = true;
1352         it.velocity = '0 0 0';
1353         set_movetype(it, MOVETYPE_FLY);
1354         it.flags &= ~FL_PICKUPITEMS;
1355         it.flags |= FL_NOTARGET;
1356         it.dphitcontentsmask &= ~DPCONTENTS_BODY;
1357         it.effects |= EF_NODRAW;
1358         it.takedamage = DAMAGE_NO;
1359         it.solid = SOLID_NOT;
1360         it.br_drop_instructions = false;
1361         it.br_drop_launch = 0;
1362         UNSET_ONGROUND(it); // otherwise this isn't unset if the player drops in the same frame
1363
1364         WaypointSprite_Spawn(WP_BRAlly, 0, 0, it, '0 0 64', NULL, 0, it, br_allywaypoint, true, RADARICON_WAYPOINT);
1365     });
1366
1367     squads_colored = autocvar_g_br_squad_colors;
1368
1369     FOREACH_CLIENT(IS_REAL_CLIENT(it),
1370     {
1371         br_SendSquad(it);
1372         STAT(SQUADCOLORS, it) = squads_colored;
1373     });
1374
1375     IL_EACH(squads, true,
1376     {
1377         if(squads_colored)
1378         {
1379             float squad_color;
1380             squad_color = 16 * floor(random() * 15) + floor(random() * 15); // color 15 is special, don't select it as a squad color
1381
1382             for(entity member = it.br_squad_first; member; member = member.br_squad_next)
1383             {
1384                 member.clientcolors = 1024 + squad_color;
1385             }
1386         }
1387
1388         it.br_drop_time = time;
1389
1390         float min_distance = max(autocvar_g_br_drop_distance_force, 0);
1391         if(!br_SquadIsBotsOnly(it))
1392             it.br_force_drop_distance = min_distance;
1393         else
1394             it.br_force_drop_distance = min_distance + random() * max(dropship_path_length - (min_distance + dropship_speed * br_drop_time_secs), 0);
1395     });
1396
1397     br_event_supply_time = br_event_vehicle_time = time;
1398     round_handler.cnt = 0; // emulate round handler round start
1399 }
1400
1401 void br_dummy_Think(entity this){}
1402
1403 void br_Initialize()
1404 {
1405     br_started = false;
1406     squads_colored = autocvar_g_br_squad_colors;
1407
1408     // emulate the round handler, useful because this will cause a lot of code to correctly treat the stage before the match starts
1409     round_handler = new_pure(round_handler);
1410     round_handler.count = 0;
1411     round_handler.cnt = 1;
1412     round_handler.wait = false;
1413     setthink(round_handler, br_dummy_Think);
1414
1415     EliminatedPlayers_Init(br_isEliminated);
1416 }