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