From 7f0fc99c65ee9e5df7c293c7fb58464ff488aa98 Mon Sep 17 00:00:00 2001 From: Juhu <5894800-Juhu_@users.noreply.gitlab.com> Date: Mon, 17 Jan 2022 03:35:04 +0100 Subject: [PATCH] implement battle royale gamemode in xonotic --- _hud_descriptions.cfg | 1 + balance-mario.cfg | 8 + balance-nexuiz25.cfg | 8 + balance-overkill.cfg | 8 + balance-samual.cfg | 8 + balance-xdf.cfg | 8 + balance-xonotic.cfg | 8 + balance-xpm.cfg | 8 + gamemodes-client.cfg | 1 + gamemodes-server.cfg | 43 + hud_luma.cfg | 1 + hud_luminos.cfg | 1 + hud_luminos_minimal.cfg | 1 + hud_luminos_minimal_xhair.cfg | 1 + hud_luminos_old.cfg | 1 + hud_nexuiz.cfg | 1 + qcsrc/client/hud/panel/scoreboard.qc | 12 +- qcsrc/common/deathtypes/all.inc | 1 + qcsrc/common/ent_cs.qc | 1 + qcsrc/common/gamemodes/gamemode/_mod.inc | 1 + qcsrc/common/gamemodes/gamemode/_mod.qh | 1 + qcsrc/common/gamemodes/gamemode/br/_mod.inc | 21 + qcsrc/common/gamemodes/gamemode/br/_mod.qh | 21 + qcsrc/common/gamemodes/gamemode/br/br.qc | 5 + qcsrc/common/gamemodes/gamemode/br/br.qh | 41 + qcsrc/common/gamemodes/gamemode/br/cl_br.qc | 124 +++ qcsrc/common/gamemodes/gamemode/br/cl_br.qh | 7 + qcsrc/common/gamemodes/gamemode/br/cl_ring.qc | 134 +++ qcsrc/common/gamemodes/gamemode/br/cl_ring.qh | 1 + qcsrc/common/gamemodes/gamemode/br/ring.qc | 33 + qcsrc/common/gamemodes/gamemode/br/ring.qh | 18 + qcsrc/common/gamemodes/gamemode/br/sv_br.qc | 986 ++++++++++++++++++ qcsrc/common/gamemodes/gamemode/br/sv_br.qh | 36 + .../gamemodes/gamemode/br/sv_dropship.qc | 162 +++ .../gamemodes/gamemode/br/sv_dropship.qh | 9 + qcsrc/common/gamemodes/gamemode/br/sv_ring.qc | 219 ++++ qcsrc/common/gamemodes/gamemode/br/sv_ring.qh | 4 + .../common/gamemodes/gamemode/br/sv_squad.qc | 160 +++ .../common/gamemodes/gamemode/br/sv_squad.qh | 30 + qcsrc/common/mapinfo.qc | 2 +- .../common/mutators/mutator/waypoints/all.inc | 5 + qcsrc/common/notifications/all.inc | 20 + qcsrc/common/notifications/all.qh | 2 + qcsrc/common/scores.qh | 4 + qcsrc/common/stats.qh | 4 + qcsrc/lib/csqcmodel/cl_player.qc | 2 +- qcsrc/menu/xonotic/util.qc | 1 + qcsrc/server/command/cmd.qc | 2 +- qcsrc/server/damage.qc | 10 +- qcsrc/server/world.qc | 1 + 50 files changed, 2174 insertions(+), 13 deletions(-) create mode 100644 qcsrc/common/gamemodes/gamemode/br/_mod.inc create mode 100644 qcsrc/common/gamemodes/gamemode/br/_mod.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/br.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/br.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/cl_br.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/cl_br.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/cl_ring.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/cl_ring.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/ring.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/ring.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_br.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_br.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_dropship.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_ring.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_ring.qh create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_squad.qc create mode 100644 qcsrc/common/gamemodes/gamemode/br/sv_squad.qh diff --git a/_hud_descriptions.cfg b/_hud_descriptions.cfg index 02d2d5a0f..eaf4a7fe4 100644 --- a/_hud_descriptions.cfg +++ b/_hud_descriptions.cfg @@ -198,6 +198,7 @@ seta hud_panel_modicons_bg_padding "" "if set to something else than \"\" = over seta hud_panel_modicons_ca_layout "" "2 possible layouts: 0) number of alive players; 1) icons and number of alive players" seta hud_panel_modicons_dom_layout "" "3 possible layouts: 0) only icons; 1) icons and percentage of average pps (points per second); 2) icons and average pps" seta hud_panel_modicons_freezetag_layout "" "2 possible layouts: 0) number of alive players; 1) icons and number of alive players" +seta hud_panel_modicons_br_layout "" "2 possible layouts: 0) number of alive squads/players; 1) icons and number of alive squads/players" seta hud_panel_pressedkeys_pos "" "position of this base of the panel" seta hud_panel_pressedkeys_size "" "size of this panel" diff --git a/balance-mario.cfg b/balance-mario.cfg index 42e3a6696..325db8e58 100644 --- a/balance-mario.cfg +++ b/balance-mario.cfg @@ -49,6 +49,14 @@ set g_lms_start_ammo_rockets 160 set g_lms_start_ammo_cells 180 set g_lms_start_ammo_plasma 180 set g_lms_start_ammo_fuel 0 +set g_br_start_health 100 +set g_br_start_armor 0 +set g_br_start_ammo_shells 0 +set g_br_start_ammo_nails 0 +set g_br_start_ammo_rockets 0 +set g_br_start_ammo_cells 0 +set g_br_start_ammo_plasma 0 +set g_br_start_ammo_fuel 0 set g_balance_nix_roundtime 25 set g_balance_nix_incrtime 1.6 set g_balance_nix_ammo_shells 60 diff --git a/balance-nexuiz25.cfg b/balance-nexuiz25.cfg index 6cdc29dcc..d34e8e309 100644 --- a/balance-nexuiz25.cfg +++ b/balance-nexuiz25.cfg @@ -49,6 +49,14 @@ set g_lms_start_ammo_rockets 50 set g_lms_start_ammo_cells 50 set g_lms_start_ammo_plasma 50 set g_lms_start_ammo_fuel 0 +set g_br_start_health 150 +set g_br_start_armor 0 +set g_br_start_ammo_shells 0 +set g_br_start_ammo_nails 0 +set g_br_start_ammo_rockets 0 +set g_br_start_ammo_cells 0 +set g_br_start_ammo_plasma 0 +set g_br_start_ammo_fuel 0 set g_balance_nix_roundtime 25 set g_balance_nix_incrtime 1.6 set g_balance_nix_ammo_shells 15 diff --git a/balance-overkill.cfg b/balance-overkill.cfg index e33ee827f..752a89825 100644 --- a/balance-overkill.cfg +++ b/balance-overkill.cfg @@ -49,6 +49,14 @@ set g_lms_start_ammo_rockets 160 set g_lms_start_ammo_cells 180 set g_lms_start_ammo_plasma 180 set g_lms_start_ammo_fuel 0 +set g_br_start_health 100 +set g_br_start_armor 0 +set g_br_start_ammo_shells 0 +set g_br_start_ammo_nails 0 +set g_br_start_ammo_rockets 0 +set g_br_start_ammo_cells 0 +set g_br_start_ammo_plasma 0 +set g_br_start_ammo_fuel 0 set g_balance_nix_roundtime 25 set g_balance_nix_incrtime 1.6 set g_balance_nix_ammo_shells 60 diff --git a/balance-samual.cfg b/balance-samual.cfg index 7850aaba8..44b110586 100644 --- a/balance-samual.cfg +++ b/balance-samual.cfg @@ -49,6 +49,14 @@ set g_lms_start_ammo_rockets 160 set g_lms_start_ammo_cells 180 set g_lms_start_ammo_plasma 180 set g_lms_start_ammo_fuel 0 +set g_br_start_health 100 +set g_br_start_armor 0 +set g_br_start_ammo_shells 0 +set g_br_start_ammo_nails 0 +set g_br_start_ammo_rockets 0 +set g_br_start_ammo_cells 0 +set g_br_start_ammo_plasma 0 +set g_br_start_ammo_fuel 0 set g_balance_nix_roundtime 25 set g_balance_nix_incrtime 1.6 set g_balance_nix_ammo_shells 60 diff --git a/balance-xdf.cfg b/balance-xdf.cfg index 94e98a646..3bc9cc7aa 100644 --- a/balance-xdf.cfg +++ b/balance-xdf.cfg @@ -49,6 +49,14 @@ set g_lms_start_ammo_rockets 160 set g_lms_start_ammo_cells 180 set g_lms_start_ammo_plasma 180 set g_lms_start_ammo_fuel 0 +set g_br_start_health 100 +set g_br_start_armor 0 +set g_br_start_ammo_shells 0 +set g_br_start_ammo_nails 0 +set g_br_start_ammo_rockets 0 +set g_br_start_ammo_cells 0 +set g_br_start_ammo_plasma 0 +set g_br_start_ammo_fuel 0 set g_balance_nix_roundtime 25 set g_balance_nix_incrtime 1.6 set g_balance_nix_ammo_shells 60 diff --git a/balance-xonotic.cfg b/balance-xonotic.cfg index 784e447f4..ba3bf0b5a 100644 --- a/balance-xonotic.cfg +++ b/balance-xonotic.cfg @@ -49,6 +49,14 @@ set g_lms_start_ammo_rockets 160 set g_lms_start_ammo_cells 180 set g_lms_start_ammo_plasma 180 set g_lms_start_ammo_fuel 0 +set g_br_start_health 100 +set g_br_start_armor 0 +set g_br_start_ammo_shells 0 +set g_br_start_ammo_nails 0 +set g_br_start_ammo_rockets 0 +set g_br_start_ammo_cells 0 +set g_br_start_ammo_plasma 0 +set g_br_start_ammo_fuel 0 set g_balance_nix_roundtime 25 set g_balance_nix_incrtime 1.6 set g_balance_nix_ammo_shells 60 diff --git a/balance-xpm.cfg b/balance-xpm.cfg index 189be9c71..ba72b43db 100644 --- a/balance-xpm.cfg +++ b/balance-xpm.cfg @@ -49,6 +49,14 @@ set g_lms_start_ammo_rockets 160 set g_lms_start_ammo_cells 180 set g_lms_start_ammo_plasma 180 set g_lms_start_ammo_fuel 0 +set g_br_start_health 100 +set g_br_start_armor 0 +set g_br_start_ammo_shells 0 +set g_br_start_ammo_nails 0 +set g_br_start_ammo_rockets 0 +set g_br_start_ammo_cells 0 +set g_br_start_ammo_plasma 0 +set g_br_start_ammo_fuel 0 set g_balance_nix_roundtime 25 set g_balance_nix_incrtime 1.6 set g_balance_nix_ammo_shells 60 diff --git a/gamemodes-client.cfg b/gamemodes-client.cfg index c43b9d1d3..cf3e35b69 100644 --- a/gamemodes-client.cfg +++ b/gamemodes-client.cfg @@ -32,6 +32,7 @@ alias cl_hook_gamestart_ka alias cl_hook_gamestart_ft alias cl_hook_gamestart_inv alias cl_hook_gamestart_duel +alias cl_hook_gamestart_br alias cl_hook_gameend "rpn /cl_matchcount dup load 1 + =" // increase match count every time a game ends alias cl_hook_shutdown alias cl_hook_activeweapon diff --git a/gamemodes-server.cfg b/gamemodes-server.cfg index 17b90c624..259209f09 100644 --- a/gamemodes-server.cfg +++ b/gamemodes-server.cfg @@ -29,6 +29,7 @@ alias sv_hook_gamestart_ka alias sv_hook_gamestart_ft alias sv_hook_gamestart_inv alias sv_hook_gamestart_duel +alias sv_hook_gamestart_br // there is currently no hook for when the match is restarted // see sv_hook_readyrestart for previous uses of this hook //alias sv_hook_gamerestart @@ -58,6 +59,7 @@ alias sv_vote_gametype_hook_ons alias sv_vote_gametype_hook_rc alias sv_vote_gametype_hook_tdm alias sv_vote_gametype_hook_duel +alias sv_vote_gametype_hook_br // Example preset to allow 1v1ctf to be used for the gametype voting screen. // Aliases can have max 31 chars so the gametype can have max 9 chars. @@ -208,6 +210,13 @@ set g_duel_respawn_delay_large_count 0 set g_duel_respawn_delay_max 0 set g_duel_respawn_waves 0 set g_duel_weapon_stay 0 +set g_br_respawn_delay_small 0 +set g_br_respawn_delay_small_count 0 +set g_br_respawn_delay_large 0 +set g_br_respawn_delay_large_count 0 +set g_br_respawn_delay_max 0 +set g_br_respawn_waves 0 +set g_br_weapon_stay 0 // ========= @@ -559,3 +568,37 @@ set g_duel 0 "Duel: frag the opponent more in a one versus one arena battle" //set g_duel_warmup 180 "Have a short warmup period before beginning the actual duel" set g_duel_with_powerups 0 "Enable powerups to spawn in the duel gamemode" set g_duel_not_dm_maps 0 "when this is set, DM maps will NOT be listed in duel" + +// ====== +// battle royale +// ====== +set g_br 0 "Battle Royale: survive on a shrinking battlefield" +set g_br_minplayers 2 +set g_br_squad_size 3 +set g_br_squad_colors 1 +set g_br_startweapons 0 +set g_br_bleed 0.02 +set g_br_bleedlinear 1 +set g_br_bleeding_health 0.5 +set g_br_revive_speed 0.4 +set g_br_revive_clearspeed 1.6 +set g_br_revive_extra_size 100 +set g_br_revive_health 0.25 +set g_br_dropship_color "0.5 0 0.5" +set g_br_dropship_scale 3 +set g_br_dropship_speed 200 +set g_br_drop_speed_max 2 +set g_br_drop_speed_horizontal_max 0.9 +set g_br_drop_speed_horizontal_min 0.5 +set g_br_drop_speed_vertical 1.5 +set g_br_drop_acceleration 1200 +set g_br_drop_distance_force 500 +set g_br_ring_alpha 0.5 +set g_br_ring_color "1 0 0" +set g_br_ring_radius -1 +set g_br_ring_duration 150 +set g_br_ring_timing "0.6 0.8 0.9" +set g_br_ring_strength "2.5 5 10 20 50" +set g_br_ring_wait 30 +set g_br_ring_center_factor 0.25 +set g_br_ring_fadedistance 2000 diff --git a/hud_luma.cfg b/hud_luma.cfg index 7bcbef2b9..e346d41da 100644 --- a/hud_luma.cfg +++ b/hud_luma.cfg @@ -199,6 +199,7 @@ seta hud_panel_modicons_bg_padding "0" seta hud_panel_modicons_ca_layout "1" seta hud_panel_modicons_dom_layout "1" seta hud_panel_modicons_freezetag_layout "1" +seta hud_panel_modicons_br_layout "1" seta hud_panel_pressedkeys_pos "0.445000 0.710000" seta hud_panel_pressedkeys_size "0.110000 0.090000" diff --git a/hud_luminos.cfg b/hud_luminos.cfg index 715d57efd..769cbae05 100644 --- a/hud_luminos.cfg +++ b/hud_luminos.cfg @@ -199,6 +199,7 @@ seta hud_panel_modicons_bg_padding "0" seta hud_panel_modicons_ca_layout "1" seta hud_panel_modicons_dom_layout "1" seta hud_panel_modicons_freezetag_layout "1" +seta hud_panel_modicons_br_layout "1" seta hud_panel_pressedkeys_pos "0.450000 0.720000" seta hud_panel_pressedkeys_size "0.110000 0.090000" diff --git a/hud_luminos_minimal.cfg b/hud_luminos_minimal.cfg index 25f871bf1..84e53e538 100644 --- a/hud_luminos_minimal.cfg +++ b/hud_luminos_minimal.cfg @@ -199,6 +199,7 @@ seta hud_panel_modicons_bg_padding "" seta hud_panel_modicons_ca_layout "1" seta hud_panel_modicons_dom_layout "1" seta hud_panel_modicons_freezetag_layout "1" +seta hud_panel_modicons_br_layout "1" seta hud_panel_pressedkeys_pos "0.450000 0.650000" seta hud_panel_pressedkeys_size "0.100000 0.110000" diff --git a/hud_luminos_minimal_xhair.cfg b/hud_luminos_minimal_xhair.cfg index e3e857b0a..6f4d92eb2 100644 --- a/hud_luminos_minimal_xhair.cfg +++ b/hud_luminos_minimal_xhair.cfg @@ -199,6 +199,7 @@ seta hud_panel_modicons_bg_padding "" seta hud_panel_modicons_ca_layout "1" seta hud_panel_modicons_dom_layout "1" seta hud_panel_modicons_freezetag_layout "1" +seta hud_panel_modicons_br_layout "1" seta hud_panel_pressedkeys_pos "0.450000 0.690000" seta hud_panel_pressedkeys_size "0.100000 0.110000" diff --git a/hud_luminos_old.cfg b/hud_luminos_old.cfg index fdf1efff3..62df37427 100644 --- a/hud_luminos_old.cfg +++ b/hud_luminos_old.cfg @@ -199,6 +199,7 @@ seta hud_panel_modicons_bg_padding "" seta hud_panel_modicons_ca_layout "1" seta hud_panel_modicons_dom_layout "1" seta hud_panel_modicons_freezetag_layout "1" +seta hud_panel_modicons_br_layout "1" seta hud_panel_pressedkeys_pos "0.410000 0.710000" seta hud_panel_pressedkeys_size "0.180000 0.130000" diff --git a/hud_nexuiz.cfg b/hud_nexuiz.cfg index 74132c08a..85541b69d 100644 --- a/hud_nexuiz.cfg +++ b/hud_nexuiz.cfg @@ -199,6 +199,7 @@ seta hud_panel_modicons_bg_padding "" seta hud_panel_modicons_ca_layout "1" seta hud_panel_modicons_dom_layout "1" seta hud_panel_modicons_freezetag_layout "1" +seta hud_panel_modicons_br_layout "1" seta hud_panel_pressedkeys_pos "0.440000 0.760000" seta hud_panel_pressedkeys_size "0.120000 0.094368" diff --git a/qcsrc/client/hud/panel/scoreboard.qc b/qcsrc/client/hud/panel/scoreboard.qc index b3d24e59d..1e125f6f2 100644 --- a/qcsrc/client/hud/panel/scoreboard.qc +++ b/qcsrc/client/hud/panel/scoreboard.qc @@ -392,12 +392,12 @@ void Cmd_Scoreboard_Help() // otherwise the previous exclusive rule warns anyway // e.g. -teams,rc,cts,lms/kills ?+rc/kills #define SCOREBOARD_DEFAULT_COLUMNS \ -"ping pl fps name |" \ +"ping pl fps +br/squad name |" \ " -teams,rc,cts,inv,lms/kills +ft,tdm/kills ?+rc,inv/kills" \ -" -teams,lms/deaths +ft,tdm/deaths" \ +" -teams,lms,br/deaths +ft,tdm/deaths" \ " +tdm/sum" \ -" -teams,lms,rc,cts,inv,ka/suicides +ft,tdm/suicides ?+rc,inv/suicides" \ -" -cts,dm,tdm,ka,ft/frags" /* tdm already has this in "score" */ \ +" -teams,lms,rc,cts,inv,ka,br/suicides +ft,tdm/suicides ?+rc,inv/suicides" \ +" -cts,dm,tdm,ka,ft,br/frags" /* tdm already has this in "score" */ \ " +tdm,ft,dom,ons,as/teamkills"\ " -rc,cts,nb/dmg -rc,cts,nb/dmgtaken" \ " +ctf/pickups +ctf/fckills +ctf/returns +ctf/caps +ons/takes +ons/caps" \ @@ -407,7 +407,9 @@ void Cmd_Scoreboard_Help() " +as/objectives +nb/faults +nb/goals" \ " +ka/pickups +ka/bckills +ka/bctime +ft/revivals" \ " +dom/ticks +dom/takes" \ -" -lms,rc,cts,inv,nb/score" +" -lms,rc,cts,inv,nb,br/score" \ +" +br/revivals" \ +" +br/rank" void Cmd_Scoreboard_SetFields(int argc) { diff --git a/qcsrc/common/deathtypes/all.inc b/qcsrc/common/deathtypes/all.inc index c11e5fedf..d9728d3a2 100644 --- a/qcsrc/common/deathtypes/all.inc +++ b/qcsrc/common/deathtypes/all.inc @@ -10,6 +10,7 @@ REGISTER_DEATHTYPE(GENERIC, DEATH_SELF_GENERIC, NULL REGISTER_DEATHTYPE(HURTTRIGGER, DEATH_SELF_VOID, DEATH_MURDER_VOID, "") REGISTER_DEATHTYPE(KILL, DEATH_SELF_SUICIDE, NULL, "") REGISTER_DEATHTYPE(LAVA, DEATH_SELF_LAVA, DEATH_MURDER_LAVA, "") +REGISTER_DEATHTYPE(RING, DEATH_SELF_RING, NULL, "") REGISTER_DEATHTYPE(MIRRORDAMAGE, DEATH_SELF_BETRAYAL, NULL, "") REGISTER_DEATHTYPE(MONSTER_MAGE, DEATH_SELF_MON_MAGE, DEATH_MURDER_MONSTER, "monster") REGISTER_DEATHTYPE(MONSTER_SHAMBLER_CLAW, DEATH_SELF_MON_SHAMBLER_CLAW, DEATH_MURDER_MONSTER, "monster") diff --git a/qcsrc/common/ent_cs.qc b/qcsrc/common/ent_cs.qc index a3691386b..b976d651e 100644 --- a/qcsrc/common/ent_cs.qc +++ b/qcsrc/common/ent_cs.qc @@ -188,6 +188,7 @@ ENTCS_PROP(SOLID, true, sv_solid, solid, ENTCS_SET_NORMAL, { if (radar_showenemies) break; if (SAME_TEAM(to, player)) break; + if (SAME_SQUAD(to, player)) break; if (!(IS_PLAYER(to) || to.caplayer)) break; } sf &= ENTCS_PUBLICMASK; // no private updates diff --git a/qcsrc/common/gamemodes/gamemode/_mod.inc b/qcsrc/common/gamemodes/gamemode/_mod.inc index a33ec87a0..bbc986ea4 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.inc +++ b/qcsrc/common/gamemodes/gamemode/_mod.inc @@ -1,6 +1,7 @@ // generated file; do not modify #include +#include #include #include #include diff --git a/qcsrc/common/gamemodes/gamemode/_mod.qh b/qcsrc/common/gamemodes/gamemode/_mod.qh index ffd71d59d..8a73e4945 100644 --- a/qcsrc/common/gamemodes/gamemode/_mod.qh +++ b/qcsrc/common/gamemodes/gamemode/_mod.qh @@ -1,6 +1,7 @@ // generated file; do not modify #include +#include #include #include #include diff --git a/qcsrc/common/gamemodes/gamemode/br/_mod.inc b/qcsrc/common/gamemodes/gamemode/br/_mod.inc new file mode 100644 index 000000000..42767d900 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/_mod.inc @@ -0,0 +1,21 @@ +// generated file; do not modify +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif +#ifdef SVQC + #include +#endif +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/br/_mod.qh b/qcsrc/common/gamemodes/gamemode/br/_mod.qh new file mode 100644 index 000000000..9462da4b0 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/_mod.qh @@ -0,0 +1,21 @@ +// generated file; do not modify +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif +#include +#ifdef CSQC + #include +#endif +#ifdef SVQC + #include +#endif +#ifdef SVQC + #include +#endif +#ifdef SVQC + #include +#endif diff --git a/qcsrc/common/gamemodes/gamemode/br/br.qc b/qcsrc/common/gamemodes/gamemode/br/br.qc new file mode 100644 index 000000000..1a0663f14 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/br.qc @@ -0,0 +1,5 @@ +#include "br.qh" + +#ifdef GAMEQC +REGISTER_NET_LINKED(ENT_CLIENT_RING) +#endif diff --git a/qcsrc/common/gamemodes/gamemode/br/br.qh b/qcsrc/common/gamemodes/gamemode/br/br.qh new file mode 100644 index 000000000..c6ad354bc --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/br.qh @@ -0,0 +1,41 @@ +#pragma once + +#include + +#ifdef CSQC +void HUD_Mod_BR(vector pos, vector mySize); +void HUD_Mod_BR_Export(int fh); +#endif +#ifdef SVQC +IntrusiveList squads; +#endif +CLASS(BattleRoyale, Gametype) +#ifdef SVQC + STATIC_INIT(BattleRoyale) + { + squads = IL_NEW(); + } +#endif + INIT(BattleRoyale) + { + this.gametype_init(this, _("Battle Royale"),"br","g_br",GAMETYPE_FLAG_HIDELIMITS,"","",_("Survive and kill until there are no enemies left")); + } + METHOD(BattleRoyale, m_isAlwaysSupported, bool(Gametype this, int spawnpoints, float diameter)) + { + if(diameter > 4096) + return true; + return false; + } +#ifdef CSQC + ATTRIB(BattleRoyale, m_modicons, void(vector pos, vector mySize), HUD_Mod_BR); + ATTRIB(BattleRoyale, m_modicons_export, void(int fh), HUD_Mod_BR_Export); +#endif +ENDCLASS(BattleRoyale) +REGISTER_GAMETYPE(BR, NEW(BattleRoyale)); +#define g_br IS_GAMETYPE(BR) + +#ifdef GAMEQC +const int DROP_LANDED = 0; +const int DROP_FALLING = 1; +const int DROP_TRANSPORT = 2; +#endif diff --git a/qcsrc/common/gamemodes/gamemode/br/cl_br.qc b/qcsrc/common/gamemodes/gamemode/br/cl_br.qc new file mode 100644 index 000000000..3dddb9698 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/cl_br.qc @@ -0,0 +1,124 @@ +#include "cl_br.qh" + +#include + +REGISTER_MUTATOR(cl_br, true); + +MUTATOR_HOOKFUNCTION(cl_br, WantEventchase) +{ + return (STAT(DROP) == DROP_FALLING); +} + +MUTATOR_HOOKFUNCTION(cl_br, DrawCrosshair) +{ + return (STAT(BLEEDING) || (STAT(DROP) != DROP_LANDED)); +} + +MUTATOR_HOOKFUNCTION(cl_br, PlayerCanCrouch) +{ + if(STAT(BLEEDING)) + M_ARGV(1, bool) = true; + else if(STAT(DROP) != DROP_LANDED) + M_ARGV(1, bool) = false; +} + +MUTATOR_HOOKFUNCTION(cl_br, PlayerJump) +{ + return STAT(BLEEDING) || (STAT(DROP) != DROP_LANDED); +} + +MUTATOR_HOOKFUNCTION(cl_br, PM_Physics) +{ + if(STAT(DROP) == DROP_FALLING) + ITEMS_STAT(csqcplayer) |= IT_USING_JETPACK; + + return (STAT(DROP) != DROP_LANDED); +} + +// adjusted clanarena hud +#include + +void HUD_Mod_BR_Export(int fh) +{ + HUD_Write_Cvar("hud_panel_modicons_br_layout"); +} + +void DrawBRItem(vector myPos, vector mySize, float aspect_ratio, int layout, int i) +{ + TC(int, layout); TC(int, i); + int stat = -1; + vector color = '0 0 0'; + switch(i) + { + case 0: stat = STAT(SQUADSALIVE); color = '1 1 0'; break; + default: + case 1: stat = STAT(PLAYERSALIVE); color = '1 0 0'; break; + } + + float aspect_size; + if(mySize.x/mySize.y > aspect_ratio) + { + aspect_size = aspect_ratio * mySize.y; + myPos.x = myPos.x + (mySize.x - aspect_size) / 2; + mySize.x = aspect_size; + } + else + { + aspect_size = 1/aspect_ratio * mySize.x; + myPos.y = myPos.y + (mySize.y - aspect_size) / 2; + mySize.y = aspect_size; + } + + if(layout) + { + if(i == 0) + { + drawpic_aspect_skin(myPos + eX / 12 * mySize.x - eY / 12 * mySize.y, "player_blue", vec2(mySize.x / 3, mySize.y), '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL); + drawpic_aspect_skin(myPos + eY / 12 * mySize.y, "player_yellow", vec2(mySize.x / 3, mySize.y), '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL); + drawpic_aspect_skin(myPos + eX / 6 * mySize.x + eY / 12 * mySize.y, "player_pink", vec2(mySize.x / 3, mySize.y), '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL); + } + else + { + drawpic_aspect_skin(myPos, "player_red", vec2(mySize.x / 2, mySize.y), '1 1 1', panel_fg_alpha, DRAWFLAG_NORMAL); + } + drawstring_aspect(myPos + eX * mySize.x / 2, ftos(stat), vec2(mySize.x / 2, mySize.y), color, panel_fg_alpha, DRAWFLAG_NORMAL); + } + else + drawstring_aspect(myPos, ftos(stat), mySize, color, panel_fg_alpha, DRAWFLAG_NORMAL); +} + +void HUD_Mod_BR_Draw(vector myPos, vector mySize, int layout) +{ + int rows, columns; + float aspect_ratio; + aspect_ratio = (layout) ? 2 : 1; + rows = HUD_GetRowCount(2, mySize, aspect_ratio); + columns = ceil(2/rows); + + int i; + float row = 0, column = 0; + vector pos = '0 0 0', itemSize; + itemSize = vec2(mySize.x / columns, mySize.y / rows); + for(i=0; i<2; ++i) + { + pos.x = myPos.x + column * itemSize.x; + pos.y = myPos.y + row * itemSize.y; + + DrawBRItem(pos, itemSize, aspect_ratio, layout, i); + + ++row; + if(row >= rows) + { + row = 0; + ++column; + } + } +} + +// Battle Royale HUD modicons +void HUD_Mod_BR(vector myPos, vector mySize) +{ + mod_active = 1; // required in each mod function that always shows something + + HUD_Mod_BR_Draw(myPos, mySize, autocvar_hud_panel_modicons_br_layout); +} diff --git a/qcsrc/common/gamemodes/gamemode/br/cl_br.qh b/qcsrc/common/gamemodes/gamemode/br/cl_br.qh new file mode 100644 index 000000000..faec2409d --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/cl_br.qh @@ -0,0 +1,7 @@ +#pragma once + +int autocvar_hud_panel_modicons_br_layout = 1; + +void HUD_Mod_BR(vector myPos, vector mySize); +void HUD_Mod_BR_Draw(vector myPos, vector mySize, int layout); +void HUD_Mod_BR_Export(int fh); diff --git a/qcsrc/common/gamemodes/gamemode/br/cl_ring.qc b/qcsrc/common/gamemodes/gamemode/br/cl_ring.qc new file mode 100644 index 000000000..609cb232c --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/cl_ring.qc @@ -0,0 +1,134 @@ +#include "cl_ring.qh" + +// TODO: support dark colors + +bool ring_is_closing(entity this); + +float autocvar_g_br_ring_fadedistance = 2000; + +#define RING_MODEL_PATH "models/sphere/sphere.md3" +#define RING_MODEL_RADIUS 64 // radius of model at 1.0 scale +void ring_draw(entity this) +{ + float current_radius = ring_calculate_current_radius(this); + + if(ring_is_closing(this)) // only move when we actually should, increases visual fidelity in case the server lags and sends the wait signal late + { + setorigin(this, this.origin + this.velocity * (time - this.move_time)); // 3D is drawn before 2D, the origin will also be used in ring_draw2d afterwards + this.move_time = time; + } + + if(current_radius <= 0) + { + setmodel(this, MDL_Null); + this.scale = this.alpha = 0; + this.velocity = '0 0 0'; + this.draw = func_null; + return; + } + + this.scale = current_radius / RING_MODEL_RADIUS; + this.alpha = max((1 - min(max(current_radius - vlen((csqcplayer.origin + csqcplayer.view_ofs) - this.origin), 0) / max(autocvar_g_br_ring_fadedistance, 1), 1)) * this.br_ring_alpha, 0.01); + if(vlen((csqcplayer.origin + csqcplayer.view_ofs) - this.origin) > current_radius) + this.alpha = max(this.alpha / 4, 0.01); // let's weaken the ring visuals a bit, everything is already ring colored +} + +void ring_draw2d(entity this) +{ + float current_radius = ring_calculate_current_radius(this); + + if(vlen((csqcplayer.origin + csqcplayer.view_ofs) - this.origin) > current_radius) + { + R_BeginPolygon("", DRAWFLAG_ADDITIVE, true); + R_PolygonVertex('0 0 0', '0 0 0', this.colormod, min(this.br_ring_alpha, 0.5)); + R_PolygonVertex(autocvar_vid_conwidth * '1 0 0', '1 0 0', this.colormod, min(this.br_ring_alpha, 0.5)); + R_PolygonVertex(autocvar_vid_conwidth * '1 0 0' + autocvar_vid_conheight * '0 1 0', '1 1 0', this.colormod, min(this.br_ring_alpha, 0.5)); + R_PolygonVertex(autocvar_vid_conheight * '0 1 0', '0 1 0', this.colormod, min(this.br_ring_alpha, 0.5)); + R_EndPolygon(); + } +} + +void ring_construct(entity this, bool isnew) +{ + this.netname = BR_RING_NAME; + + setorigin(this, this.origin); + setsize(this, '0 0 0', '0 0 0'); + set_movetype(this, MOVETYPE_NOCLIP); + _setmodel(this, RING_MODEL_PATH); + + this.classname = "ring"; + this.solid = SOLID_NOT; + this.draw = ring_draw; + this.drawmask = MASK_NORMAL; + this.draw2d = ring_draw2d; + + if(isnew) + { + IL_PUSH(g_drawables, this); + IL_PUSH(g_drawables_2d, this); + } +} + +NET_HANDLE(ENT_CLIENT_RING, bool isnew) +{ + float sf; + sf = ReadByte(); + + if(sf & BR_RING_SETUP) + { + this.origin = ReadVector(); + setorigin(this, this.origin); + + this.br_ring_start = ReadCoord(); + this.br_ring_duration = max(ReadCoord(), 1); + + this.radius = max(ReadCoord(), 0); + + this.colormod = normalize(ReadVector()); + + this.br_ring_alpha = bound(0.01, ReadCoord(), 1); + + this.br_ring_stage_count = max(ReadByte(), 1); + this.br_ring_stage_waittime = max(ReadCoord(), 0); + for(int i = 0; i < (this.br_ring_stage_count + 1); ++i) + this.br_ring_stage_timing[i] = ReadCoord(); + + ring_construct(this, isnew); + } + + if(sf & BR_RING_MOVE) + { + this.origin = ReadVector(); + setorigin(this, this.origin); + + this.velocity = ReadVector(); + + this.move_time = time; + } + + return true; +} + +bool ring_is_closing(entity this) +{ + float time_elapsed = time - this.br_ring_start; + + for(int stage = 0; stage < this.br_ring_stage_count; ++stage) + { + float stage_duration = this.br_ring_duration * this.br_ring_stage_timing[stage]; + stage_duration += this.br_ring_stage_waittime * stage; + + if(time_elapsed > stage_duration) + { + if(time_elapsed < (stage_duration + this.br_ring_stage_waittime)) + { + return false; + } + } + else + return true; + } + + return (time_elapsed <= this.br_ring_duration); +} diff --git a/qcsrc/common/gamemodes/gamemode/br/cl_ring.qh b/qcsrc/common/gamemodes/gamemode/br/cl_ring.qh new file mode 100644 index 000000000..6f70f09be --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/cl_ring.qh @@ -0,0 +1 @@ +#pragma once diff --git a/qcsrc/common/gamemodes/gamemode/br/ring.qc b/qcsrc/common/gamemodes/gamemode/br/ring.qc new file mode 100644 index 000000000..08b2690ef --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/ring.qc @@ -0,0 +1,33 @@ +#include "ring.qh" + +#ifdef GAMEQC +float ring_calculate_current_radius(entity this) +{ + float time_elapsed = time - this.br_ring_start; + float time_elapsed_minus_wait = time_elapsed; + + for(int stage = 0; stage < this.br_ring_stage_count; ++stage) + { + float stage_duration = this.br_ring_duration * this.br_ring_stage_timing[stage]; + stage_duration += this.br_ring_stage_waittime * stage; + + if(time_elapsed > stage_duration) + { + if(time_elapsed >= (stage_duration + this.br_ring_stage_waittime)) + { + time_elapsed_minus_wait -= this.br_ring_stage_waittime; + } + else + { + time_elapsed_minus_wait -= time_elapsed - stage_duration; + break; + } + } + else + break; + } + + float radius_ratio = 1 - time_elapsed_minus_wait / this.br_ring_duration; + return this.radius * radius_ratio; +} +#endif diff --git a/qcsrc/common/gamemodes/gamemode/br/ring.qh b/qcsrc/common/gamemodes/gamemode/br/ring.qh new file mode 100644 index 000000000..c15777bc0 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/ring.qh @@ -0,0 +1,18 @@ +#pragma once + +#ifdef GAMEQC +const string BR_RING_NAME = "^3the ring"; + +const int BR_RING_SETUP = 2; +const int BR_RING_MOVE = 4; + +#define BR_RING_STAGE_MAX 128 +.float br_ring_start; +.float br_ring_duration; +.float br_ring_alpha; +.int br_ring_stage_count; +.float br_ring_stage_waittime; +.float br_ring_stage_timing[BR_RING_STAGE_MAX]; + +float ring_calculate_current_radius(entity this); +#endif diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_br.qc b/qcsrc/common/gamemodes/gamemode/br/sv_br.qc new file mode 100644 index 000000000..87a527f35 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_br.qc @@ -0,0 +1,986 @@ +// battle royale +// author: Juhu + +#include "sv_br.qh" +#include +#include +#include + +#define BR_KILLS_INSTANTLY(pl, dt) (!IN_SQUAD((pl)) || (br_SquadFindLastAlive((pl).br_squad, true) == (pl)) || ((dt) == DEATH_HURTTRIGGER.m_id) || ((dt) == DEATH_KILL.m_id) || ((dt) == DEATH_TEAMCHANGE.m_id) || ((dt) == DEATH_AUTOTEAMCHANGE.m_id)) + +void br_SetPlayerDropAngle(entity this); +void br_LastPlayerForSquad_Notify(entity squad); +void br_RemovePlayer(entity player); +void br_Revive(entity player); +void br_PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force); +int br_WinningCondition(); + +entity ring; +entity dropship; + +.bool br_ring_warned; +.float br_force_drop_distance; + +.entity br_bleeding_inflictor; +.entity br_bleeding_attacker; +.int br_bleeding_deathtype; +..entity br_bleeding_weaponentity; + +// weapon set restoring for revive/drop +.WepSet br_wepset_old; +.Weapon br_weapon_prev[MAX_WEAPONSLOTS]; +.float br_lastweapon_prev[MAX_WEAPONSLOTS]; + +float autocvar_g_br_revive_health = 0.25; +float autocvar_g_br_bleeding_health = 0.5; +float autocvar_g_br_drop_speed_max = 2; +float autocvar_g_br_drop_speed_horizontal_max = 0.9; +float autocvar_g_br_drop_speed_horizontal_min = 0.5; +float autocvar_g_br_drop_speed_vertical = 1.5; +bool autocvar_g_br_squad_colors = true; +float autocvar_g_br_drop_acceleration = 1200; +bool autocvar_g_br_startweapons = false; + +.vector br_drop_velocity; +.vector br_drop_angles; + +MUTATOR_HOOKFUNCTION(br, reset_map_global) +{ + dropship_path_length = 0; // this should kill the dropship + dropship_path_direction = '0 0 0'; + + delete(ring); + ring = dropship = NULL; +} + +MUTATOR_HOOKFUNCTION(br, reset_map_players) +{ + FOREACH_CLIENT(true, { + GameRules_scoring_add(it, BR_RANK, -GameRules_scoring_add(it, BR_RANK, 0)); + GameRules_scoring_add(it, BR_SQUAD, -GameRules_scoring_add(it, BR_SQUAD, 0)); + GameRules_scoring_add(it, BR_REVIVALS, -GameRules_scoring_add(it, BR_REVIVALS, 0)); + + br_RemovePlayer(it); + + it.br_wepset_old = '0 0 0'; + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + it.br_weapon_prev[slot] = WEP_Null; + it.br_lastweapon_prev[slot] = 0; + } + }); + + br_SquadUpdateInfo(); +} + +MUTATOR_HOOKFUNCTION(br, CheckRules_World) +{ + M_ARGV(0, float) = br_WinningCondition(); + return true; +} + +MUTATOR_HOOKFUNCTION(br, ForbidPlayerScore_Clear) +{ + // don't clear player score + return true; +} + +MUTATOR_HOOKFUNCTION(br, GetPlayerStatus) +{ + entity player = M_ARGV(0, entity); + return IN_SQUAD(player); +} + +MUTATOR_HOOKFUNCTION(br, ClientConnect) +{ + if(ring) + ring_timelimit(ring); + + br_SquadUpdateInfo(); +} + +MUTATOR_HOOKFUNCTION(br, ClientDisconnect) +{ + entity player = M_ARGV(0, entity); + + br_RemovePlayer(player); + br_SquadUpdateInfo(); +} + +MUTATOR_HOOKFUNCTION(br, PutClientInServer) +{ + entity player = M_ARGV(0, entity); + + if (!(round_handler_IsActive() && round_handler_IsRoundStarted())) + return false; + + if (IN_SQUAD(player)) + { + Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_JOIN_DEAD); + if(IS_REAL_CLIENT(player) && !player.br_squad.br_squad_dead) + { + SpectateNext(player); + TRANSMUTE(Spectator, player); + } + else + { + TRANSMUTE(Observer, player); + } + } + else + { + Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_JOIN_LATE); + TRANSMUTE(Observer, player); + } +} + +MUTATOR_HOOKFUNCTION(br, MakePlayerObserver) +{ + entity player = M_ARGV(0, entity); + + return IN_SQUAD(player); +} + +MUTATOR_HOOKFUNCTION(br, SpectateSet) +{ + entity client = M_ARGV(0, entity); + entity target = M_ARGV(1, entity); + + return (IN_SQUAD(client) && !client.br_squad.br_squad_dead && DIFF_SQUAD(client, target)); +} + +MUTATOR_HOOKFUNCTION(br, SpectateNext) +{ + entity client = M_ARGV(0, entity); + + if(!IS_REAL_CLIENT(client) || !IN_SQUAD(client) || client.br_squad.br_squad_dead) + return false; + + entity new_target; + + if(client.enemy && client.enemy.br_squad_next) + new_target = client.enemy.br_squad_next; + else + new_target = client.br_squad.br_squad_first; + + while((new_target == client) || IS_DEAD(new_target) || IS_SPEC(new_target) || IS_OBSERVER(new_target)) + { + new_target = new_target.br_squad_next; + if(!new_target) + new_target = client.br_squad.br_squad_first; + } + M_ARGV(1, entity) = new_target; + + return true; +} + +MUTATOR_HOOKFUNCTION(br, SpectatePrev) +{ + entity client = M_ARGV(0, entity); + + if(!IS_REAL_CLIENT(client) || !IN_SQUAD(client) || client.br_squad.br_squad_dead) + return MUT_SPECPREV_CONTINUE; + + entity new_target; + + if(client.enemy && client.enemy.br_squad_prev) + new_target = client.enemy.br_squad_prev; + else + new_target = client.br_squad.br_squad_last; + + while((new_target == client) || IS_DEAD(new_target) || IS_SPEC(new_target) || IS_OBSERVER(new_target)) + { + new_target = new_target.br_squad_prev; + if(!new_target) + new_target = client.br_squad.br_squad_last; + } + M_ARGV(1, entity) = new_target; + + return MUT_SPECPREV_FOUND; +} + +MUTATOR_HOOKFUNCTION(br, PlayerSpawn) +{ + entity player = M_ARGV(0, entity); + + player.event_damage = br_PlayerDamage; +} + +MUTATOR_HOOKFUNCTION(br, ForbidSpawn) +{ + return (round_handler_IsActive() && round_handler_IsRoundStarted()); +} + +MUTATOR_HOOKFUNCTION(br, SetStartItems) +{ + start_items &= ~(IT_UNLIMITED_AMMO | IT_UNLIMITED_SUPERWEAPONS); + + start_health = warmup_start_health = cvar("g_br_start_health"); + start_armorvalue = warmup_start_armorvalue = cvar("g_br_start_armor"); + start_ammo_shells = warmup_start_ammo_shells = cvar("g_br_start_ammo_shells"); + start_ammo_nails = warmup_start_ammo_nails = cvar("g_br_start_ammo_nails"); + start_ammo_rockets = warmup_start_ammo_rockets = cvar("g_br_start_ammo_rockets"); + start_ammo_cells = warmup_start_ammo_cells = cvar("g_br_start_ammo_cells"); + start_ammo_plasma = warmup_start_ammo_plasma = cvar("g_br_start_ammo_plasma"); + start_ammo_fuel = warmup_start_ammo_fuel = cvar("g_br_start_ammo_fuel"); +} + +// adjusted freezetag reviving code +#ifdef IN_REVIVING_RANGE + #undef IN_REVIVING_RANGE +#endif + +#define IN_REVIVING_RANGE(player, it, revive_extra_size) \ + (it != player && !IS_DEAD(it) && !IS_SPEC(it) && !IS_OBSERVER(it) && SAME_SQUAD(it, player) \ + && boxesoverlap(player.absmin - revive_extra_size, player.absmax + revive_extra_size, it.absmin, it.absmax)) + +MUTATOR_HOOKFUNCTION(br, PlayerPreThink, CBC_ORDER_FIRST) +{ + entity player = M_ARGV(0, entity); + + if (game_stopped || !frametime || !IS_PLAYER(player)) + return true; + + if(ring) + { + if(vlen((player.origin + player.view_ofs) - ring.origin) > ring_calculate_current_radius(ring)) + { + if(!player.br_ring_warned) + { + player.br_ring_warned = true; + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_BR_RING_WARN); + } + player.event_damage(player, ring, ring, ring.strength * frametime, DEATH_RING.m_id, DMG_NOWEP, player.origin, '0 0 0'); // ring damage + } + else + { + player.br_ring_warned = false; + } + } + + if(STAT(DROP, player) == DROP_TRANSPORT){ + if(!(STAT(PRESSED_KEYS, player) & KEY_JUMP) && (dropship_path_length > player.br_squad.br_force_drop_distance)){ + player.velocity = dropship_path_direction * max(autocvar_g_br_dropship_speed, 0); + } + else{ + if(!(IN_SQUAD(player) && player.br_squad.br_squad_drop_leader)) + { + player.effects &= ~EF_NODRAW; + set_movetype(player, MOVETYPE_WALK); + Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROPSHIP); + STAT(DROP, player) = DROP_FALLING; + float maxdropspeed = PHYS_MAXAIRSPEED(player) * max(autocvar_g_br_drop_speed_max, 0); // no maxspeed_mod available here + float maxdropspeed_xy = maxdropspeed * max(autocvar_g_br_drop_speed_horizontal_max, 0); + player.velocity.x = cos(player.angles.y * DEG2RAD); + player.velocity.y = sin(player.angles.y * DEG2RAD); + player.velocity.z = 0; + + player.velocity = player.velocity * maxdropspeed_xy * 0.75; + player.velocity.z = (maxdropspeed - vlen(player.velocity)) * -max(autocvar_g_br_drop_speed_vertical, 0); + + br_SetPlayerDropAngle(player); + + // the .br_drop values aren't modified by physics and will be used by other squad members + player.br_drop_velocity = player.velocity; + player.br_drop_angles = player.angles; + + if(IN_SQUAD(player)) + { + player.br_squad.br_squad_drop_leader = player; + + vector drop_offset; + drop_offset.x = cos((player.angles.y + 90) * DEG2RAD); + drop_offset.y = sin((player.angles.y + 90) * DEG2RAD); + drop_offset.z = 0; + drop_offset = drop_offset * vlen(vec2(player.maxs - player.mins)) + drop_offset * 32; // I hope individual players never get different mins/maxs + + FOREACH_CLIENT(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (STAT(DROP, it) == DROP_TRANSPORT), { + it.effects &= ~EF_NODRAW; + set_movetype(it, MOVETYPE_WALK); + Kill_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CPID_BR_DROPSHIP); + STAT(DROP, it) = DROP_FALLING; + + setorigin(it, player.origin + drop_offset); // FIXME: this can teleport players into brushes/void + drop_offset *= 2; + + it.velocity = it.br_drop_velocity = player.velocity; + it.angles = it.br_drop_angles = player.angles; + }); + } + } + } + } + + // adjusted freezetag reviving code + entity revivers_last = NULL; + entity revivers_first = NULL; + + bool player_is_reviving = false; + bool player_is_being_revived = false; + vector revive_extra_size = '1 1 1' * max(autocvar_g_br_revive_extra_size, 0); + FOREACH_CLIENT(IS_PLAYER(it), { + // check if player is reviving anyone + if (STAT(BLEEDING, it)) + { + if (STAT(BLEEDING, player)) + continue; + if (!IN_REVIVING_RANGE(player, it, revive_extra_size)) + continue; + player_is_reviving = true; + break; + } + + if (!STAT(BLEEDING, player)) + continue; // both player and it are NOT bleeding + if (!IN_REVIVING_RANGE(player, it, revive_extra_size)) + continue; + + // found a squadmate that is reviving player + if (revivers_last) + revivers_last.chain = it; + revivers_last = it; + if (!revivers_first) + revivers_first = it; + player_is_being_revived = true; + }); + if (revivers_last) + revivers_last.chain = NULL; + + if (!player_is_being_revived) // no squadmate nearby + { + float clearspeed = max(autocvar_g_br_revive_clearspeed, 0); + if (STAT(BLEEDING, player)) + STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) - frametime * clearspeed, 1); + else if (!player_is_reviving) + STAT(REVIVE_PROGRESS, player) = 0; // reviving nobody + } + else // OK, there is at least one squadmate reviving us + { + float spd = max(autocvar_g_br_revive_speed, 0); + STAT(REVIVE_PROGRESS, player) = bound(0, STAT(REVIVE_PROGRESS, player) + frametime * spd, 1); + + if(STAT(REVIVE_PROGRESS, player) >= 1) + { + br_Revive(player); + + // EVERY squad mate nearby gets a point (even if multiple!) + for(entity it = revivers_first; it; it = it.chain) + { + GameRules_scoring_add(it, BR_REVIVALS, +1); + } + + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_BR_REVIVED, revivers_first.netname); + Send_Notification(NOTIF_ONE, revivers_first, MSG_CENTER, CENTER_BR_REVIVE, player.netname); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_REVIVED, player.netname, revivers_first.netname); + if(autocvar_sv_eventlog) + { + string revivers = ""; + for(entity it = revivers_first; it; it = it.chain) + revivers = strcat(revivers, ftos(it.playerid), ","); + revivers = substring(revivers, 0, strlen(revivers) - 1); + GameLogEcho(strcat(":br:revival:", ftos(player.playerid), ":", revivers)); + } + } + + for(entity it = revivers_first; it; it = it.chain) + STAT(REVIVE_PROGRESS, it) = STAT(REVIVE_PROGRESS, player); + } + + if (STAT(BLEEDING, player)) + { + entity player_wp = player.waypointsprite_attached; + if (player_is_being_revived) + { + WaypointSprite_UpdateSprites(player_wp, WP_BRReviving, WP_Null, WP_Null); + WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_BR_REVIVING_COLOR); + } + else + { + WaypointSprite_UpdateSprites(player_wp, WP_BRBleeding, WP_Null, WP_Null); + WaypointSprite_UpdateTeamRadar(player_wp, RADARICON_WAYPOINT, WP_BR_BLEEDING_COLOR); + } + + WaypointSprite_UpdateMaxHealth(player_wp, 1); + WaypointSprite_UpdateHealth(player_wp, STAT(REVIVE_PROGRESS, player)); + } + + return true; +} + +#undef IN_REVIVING_RANGE + +MUTATOR_HOOKFUNCTION(br, PM_Physics) +{ + entity player = M_ARGV(0, entity); + float maxspeed_mod = M_ARGV(1, float); + float dt = M_ARGV(2, float); // tick rate + + if(STAT(DROP, player) == DROP_TRANSPORT) + return true; + + // TODO: improve dropping physics + if(STAT(DROP, player) == DROP_FALLING){ + if(!IS_ONGROUND(player) && ((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) + { + ITEMS_STAT(player) |= IT_USING_JETPACK; + bool has_drop_leader = IN_SQUAD(player) && (player.br_squad.br_squad_drop_leader && (STAT(DROP, player.br_squad.br_squad_drop_leader) == DROP_FALLING)); + bool player_is_drop_leader = has_drop_leader && (player == player.br_squad.br_squad_drop_leader); + if(player_is_drop_leader || !has_drop_leader) + { + float maxdropspeed = PHYS_MAXAIRSPEED(player) * max(maxspeed_mod, 1) * max(autocvar_g_br_drop_speed_max, 0); + float maxdropspeed_xy = maxdropspeed * max(autocvar_g_br_drop_speed_horizontal_max, 0); + float mindropspeed_xy = maxdropspeed * max(autocvar_g_br_drop_speed_horizontal_min, 0); + + makevectors(player.v_angle); + vector wishvel = v_forward * CS(player).movement.x + v_right * CS(player).movement.y; + wishvel = normalize(wishvel) * min(1, vlen(wishvel) / maxdropspeed); + wishvel.x *= max(autocvar_g_br_drop_acceleration, 0); + wishvel.y *= max(autocvar_g_br_drop_acceleration, 0); + player.velocity = player.velocity + wishvel * dt; + player.velocity.z = 0; + + if(vlen(player.velocity) == 0) + { + player.velocity.x = cos(player.angles.y * DEG2RAD); + player.velocity.y = sin(player.angles.y * DEG2RAD); + player.velocity = player.velocity * mindropspeed_xy; + } + if(vlen(player.velocity) < mindropspeed_xy) + player.velocity = normalize(player.velocity) * mindropspeed_xy; + if(vlen(player.velocity) > maxdropspeed_xy) + player.velocity = normalize(player.velocity) * maxdropspeed_xy; + + player.velocity.z = (maxdropspeed - vlen(player.velocity)) * -max(autocvar_g_br_drop_speed_vertical, 0); + + br_SetPlayerDropAngle(player); + + // the .br_drop values aren't modified by physics and will be used by other squad members + player.br_drop_velocity = player.velocity; + player.br_drop_angles = player.angles; + + if(player_is_drop_leader) + { + FOREACH_CLIENT(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (STAT(DROP, it) == DROP_FALLING), { + it.velocity = it.br_drop_velocity = player.br_drop_velocity; + it.angles = it.br_drop_angles = player.br_drop_angles; + }); + } + } + else + { + player.velocity = player.br_drop_velocity; + player.angles = player.br_drop_angles; // no fixangles, only moves the player model not the player view + } + + return true; + } + else + { + STAT(DROP, player) = DROP_LANDED; + ITEMS_STAT(player) &= ~IT_USING_JETPACK; + player.flags |= FL_PICKUPITEMS; + player.dphitcontentsmask |= DPCONTENTS_BODY; + + if(autocvar_g_br_startweapons) + { + SetResource(player, RES_SHELLS, start_ammo_shells); + SetResource(player, RES_BULLETS, start_ammo_nails); + SetResource(player, RES_ROCKETS, start_ammo_rockets); + SetResource(player, RES_CELLS, start_ammo_cells); + SetResource(player, RES_PLASMA, start_ammo_plasma); + SetResource(player, RES_FUEL, start_ammo_fuel); + STAT(WEAPONS, player) = player.br_wepset_old; + + .entity weaponentity = weaponentities[0]; + W_SwitchWeapon_Force(player, w_getbestweapon(player, weaponentity), weaponentity); + } + } + } +} + +MUTATOR_HOOKFUNCTION(br, Damage_Calculate) +{ + entity target = M_ARGV(2, entity); + + if(STAT(DROP, target) == DROP_FALLING) + { + // only take half of the usual damage + M_ARGV(4, float) /= 2; + M_ARGV(5, float) /= 2; + // weapon impact has no push force while dropping + M_ARGV(6, vector) = '0 0 0'; + } +} + +MUTATOR_HOOKFUNCTION(br, PlayerDies) +{ + entity frag_attacker = M_ARGV(1, entity); + entity frag_target = M_ARGV(2, entity); + float frag_deathtype = M_ARGV(3, float); // float for some reason, breaks if changed to int + + if(!IS_PLAYER(frag_target)) + return false; + + if(STAT(DROP, frag_target) == DROP_TRANSPORT) + { + frag_target.effects &= ~EF_NODRAW; + set_movetype(frag_target, MOVETYPE_WALK); + Kill_Notification(NOTIF_ONE_ONLY, frag_target, MSG_CENTER, CPID_BR_DROPSHIP); + } + + if(STAT(DROP, frag_target) != DROP_LANDED) + { + frag_target.dphitcontentsmask |= DPCONTENTS_BODY; + STAT(DROP, frag_target) = DROP_LANDED; + } + + if(STAT(BLEEDING, frag_target) || BR_KILLS_INSTANTLY(frag_target, frag_deathtype)) + { + if(STAT(BLEEDING, frag_target)) + { + Kill_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CPID_BR_DOWN); + STAT(BLEEDING, frag_target) = false; + } + frag_target.respawn_flags = RESPAWN_SILENT | RESPAWN_FORCE; + frag_target.respawn_time = time + 2; + return true; + } + + frag_target.flags &= ~FL_PICKUPITEMS; + RemoveGrapplingHooks(frag_target); + StatusEffects_removeall(frag_target, STATUSEFFECT_REMOVE_NORMAL); + + SetResource(frag_target, RES_HEALTH, start_health * max(autocvar_g_br_bleeding_health, 0)); + SetResource(frag_target, RES_ARMOR, 0); + Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN_WAIT); + STAT(BLEEDING, frag_target) = true; + + FOREACH_CLIENT(IS_PLAYER(it), + { + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + if(it.(weaponentity).hook.aiment == frag_target) + RemoveHook(it.(weaponentity).hook); + } + }); + + frag_target.br_wepset_old = STAT(WEAPONS, frag_target); + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + frag_target.br_weapon_prev[slot] = frag_target.(weaponentity).m_switchweapon; + frag_target.br_lastweapon_prev[slot] = frag_target.(weaponentity).cnt; + } + STAT(WEAPONS, frag_target) = '0 0 0'; + + WaypointSprite_Spawn(WP_BRBleeding, 0, 0, frag_target, '0 0 64', NULL, 0, frag_target, waypointsprite_attached, true, RADARICON_WAYPOINT).br_squad = frag_target.br_squad; + + if(frag_attacker == frag_target || !frag_attacker || ITEM_DAMAGE_NEEDKILL(frag_deathtype)) + { + if(IS_PLAYER(frag_target)) + Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN_SELF); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_DOWN_SELF, frag_target.netname); + } + else + { + if(IS_PLAYER(frag_target)) + Send_Notification(NOTIF_ONE, frag_target, MSG_CENTER, CENTER_BR_DOWN, frag_attacker.netname); + Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_BR_DOWN, frag_target.netname, frag_attacker.netname); + } + + br_SquadUpdateInfo(); + + return true; +} + +MUTATOR_HOOKFUNCTION(br, PlayerDied) +{ + entity player = M_ARGV(0, entity); + if(round_handler_IsActive() && round_handler_IsRoundStarted()) + br_LastPlayerForSquad_Notify(player.br_squad); + br_SquadUpdateInfo(); +} + +MUTATOR_HOOKFUNCTION(br, ClientObituary) +{ + entity frag_target = M_ARGV(2, entity); + float frag_deathtype = M_ARGV(3, float); // float for some reason, breaks if changed to int + + if(BR_KILLS_INSTANTLY(frag_target, frag_deathtype)) + return false; + else + return !STAT(BLEEDING, frag_target); +} + +MUTATOR_HOOKFUNCTION(br, SetResource) +{ + entity player = M_ARGV(0, entity); + if(!IS_PLAYER(player)) + return false; + + int res_type = M_ARGV(1, int); + float amount = M_ARGV(2, float); + + if(STAT(BLEEDING, player) && (res_type == RES_HEALTH || res_type == RES_ARMOR)) + { + if(amount > GetResource(player, res_type)) // prevent the player from getting health or armor in any way + return true; + } +} + +MUTATOR_HOOKFUNCTION(br, GetResourceLimit) +{ + entity player = M_ARGV(0, entity); + + if(!IS_PLAYER(player) || !STAT(BLEEDING, player)) + return false; + + int res_type = M_ARGV(1, int); + + switch(res_type) + { + case RES_HEALTH: + M_ARGV(2, float) *= max(autocvar_g_br_bleeding_health, 0); + break; + case RES_ARMOR: + M_ARGV(2, float) = 0; + } +} + +MUTATOR_HOOKFUNCTION(br, PlayerRegen) +{ + entity player = M_ARGV(0, entity); + + if(STAT(BLEEDING, player)){ + M_ARGV(7, float) = max(autocvar_g_br_bleed, 0); + M_ARGV(8, float) = max(autocvar_g_br_bleedlinear, 0); + M_ARGV(2, float) = M_ARGV(10, float) = 0; + return false; + } + return true; +} + +MUTATOR_HOOKFUNCTION(br, PlayerCanCrouch) +{ + entity player = M_ARGV(0, entity); + if(STAT(BLEEDING, player)) + M_ARGV(1, bool) = true; + else if(STAT(DROP, player) != DROP_LANDED) + M_ARGV(1, bool) = false; +} + +MUTATOR_HOOKFUNCTION(br, PlayerJump) +{ + entity player = M_ARGV(0, entity); + return STAT(BLEEDING, player) || (STAT(DROP, player) != DROP_LANDED); +} + +MUTATOR_HOOKFUNCTION(br, BotShouldAttack) +{ + entity bot = M_ARGV(0, entity); + entity target = M_ARGV(1, entity); + + return SAME_SQUAD(bot, target) || (STAT(DROP, target) == DROP_TRANSPORT); +} + +MUTATOR_HOOKFUNCTION(br, AccuracyTargetValid) +{ + entity attacker = M_ARGV(0, entity); + entity target = M_ARGV(1, entity); + + if(SAME_SQUAD(attacker, target) || (STAT(DROP, target) == DROP_TRANSPORT)) + return MUT_ACCADD_INDIFFERENT; + return MUT_ACCADD_VALID; +} + +MUTATOR_HOOKFUNCTION(br, CustomizeWaypoint) +{ + entity wp = M_ARGV(0, entity); + entity player = M_ARGV(1, entity); + + if(!IS_PLAYER(player) || DIFF_SQUAD(wp, player)) + return true; +} + +MUTATOR_HOOKFUNCTION(br, ClientKill) +{ + entity player = M_ARGV(0, entity); + + if(round_handler_IsActive() && round_handler_IsRoundStarted()) + { + // no forfeiting once the game started + Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_FORFEIT); + return true; + } + + return false; +} + +MUTATOR_HOOKFUNCTION(br, ClientCommand_Spectate) +{ + entity player = M_ARGV(0, entity); + + if(round_handler_IsActive() && round_handler_IsRoundStarted()) + { + // no forfeiting once the game started + Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_FORFEIT); + return MUT_SPECCMD_RETURN; + } + return MUT_SPECCMD_CONTINUE; +} + +void br_SetPlayerDropAngle(entity this) +{ + this.angles.y = vectoangles(vec2(this.velocity)).y; + + this.angles.x = -90; + if(this.velocity.z < 0) + { + float dropspeed_xy = vlen(vec2(this.velocity)); + float dropspeed_z = fabs(this.velocity.z); + //this.angles.x -= dropspeed_z / (dropspeed_z + dropspeed_xy) * 90; // anything beyond 90 degrees flips the model on view change for some reason + this.angles.x += dropspeed_z / (dropspeed_z + dropspeed_xy) * 90; // but this looks nice too, I guess + } +} + +void br_LastPlayerForSquad_Notify(entity squad) +{ + entity player = br_SquadFindLastAlive(squad, false); + if(player) + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_ALONE); +} + +void br_RemovePlayer(entity player) +{ + br_SquadMember_Remove(player); + + FOREACH_CLIENT((it.br_bleeding_attacker == player) || (it.br_bleeding_inflictor == player), { + it.br_bleeding_attacker = it.br_bleeding_inflictor = NULL; + }); +} + +void br_Revive(entity player) +{ + if(STAT(BLEEDING, player)) + { + Kill_Notification(NOTIF_ONE, player, MSG_CENTER, CPID_BR_DOWN); + STAT(BLEEDING, player) = false; + } + player.flags |= FL_PICKUPITEMS; + SetResource(player, RES_HEALTH, start_health * max(autocvar_g_br_revive_health, 0)); + + STAT(WEAPONS, player) = player.br_wepset_old; + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + W_SwitchWeapon_Force(player, player.br_weapon_prev[slot], weaponentity); + player.(weaponentity).cnt = player.br_lastweapon_prev[slot]; + } + + player.pauseregen_finished = time + autocvar_g_balance_pause_health_regen; + + STAT(REVIVE_PROGRESS, player) = 0; + player.revival_time = time; + + WaypointSprite_Kill(player.waypointsprite_attached); + + br_SquadUpdateInfo(); +} + +void br_PlayerDamage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force) +{ + if(STAT(DROP, this) == DROP_TRANSPORT) + return; // can't take damage while on the dropship + + // do not take fall damage when landing from dropship + if(STAT(DROP, this) == DROP_FALLING) + { + switch(deathtype) + { + case DEATH_FALL.m_id: + case DEATH_SHOOTING_STAR.m_id: + return; + } + } + + if(STAT(BLEEDING, this) && this.br_bleeding_attacker) + { + inflictor = this.br_bleeding_inflictor; + attacker = this.br_bleeding_attacker; + deathtype = this.br_bleeding_deathtype; + weaponentity = this.br_bleeding_weaponentity; + } + + PlayerDamage(this, inflictor, attacker, damage, deathtype, weaponentity, hitloc, force); + + if(STAT(BLEEDING, this)) + { + this.br_bleeding_inflictor = inflictor; + this.br_bleeding_attacker = attacker; + this.br_bleeding_deathtype = deathtype; + this.br_bleeding_weaponentity = weaponentity; + + this.fixangle = false; + } +} + +int br_WinningCondition() +{ + int total_squads = br_SquadUpdateInfo(); + + if ((total_squads > 1) || !(round_handler_IsActive() && round_handler_IsRoundStarted())) + return WINNING_NEVER; + + entity winner_squad = NULL; + IL_EACH(squads, !it.br_squad_dead, winner_squad = it); + + for(entity member = winner_squad.br_squad_first; member; member = member.br_squad_next) + { + GameRules_scoring_add(member, BR_RANK, 1); + } + + return WINNING_YES; +} + +bool br_isEliminated(entity e) +{ + return (IN_SQUAD(e) && (IS_DEAD(e) || IS_SPEC(e) || IS_OBSERVER(e))); +} + +bool br_CheckWinner() +{ + return false; +} + +bool br_CheckPlayers() +{ + total_players = 0; + FOREACH_CLIENT(IS_PLAYER(it), ++total_players); + + static int prev_players = 0; + if (total_players >= autocvar_g_br_minplayers || total_players == 0) + { + if(prev_players > 0) + Kill_Notification(NOTIF_ALL, NULL, MSG_CENTER, CPID_MISSING_PLAYERS); + prev_players = 0; + return (total_players >= autocvar_g_br_minplayers); + } + + if(prev_players != total_players) + { + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_MISSING_PLAYERS, autocvar_g_br_minplayers - total_players); + prev_players = total_players; + } + + return false; +} + +void br_RoundStart(){ + ring = ring_initialize(); + + dropship = dropship_initialize(); + + if(!dropship) + { + delete(ring); + ring = NULL; + + FOREACH_CLIENT(IS_PLAYER(it), { + TRANSMUTE(Observer, it); + PutClientInServer(it); + }); + LOG_SEVERE("Failed to determine dropship route, aborting..."); + } + + int num_players = 0; + + FOREACH_CLIENT(IS_PLAYER(it), { + GameRules_scoring_add(it, BR_RANK, -GameRules_scoring_add(it, BR_RANK, 0)); + GameRules_scoring_add(it, BR_SQUAD, -GameRules_scoring_add(it, BR_SQUAD, 0)); + GameRules_scoring_add(it, BR_REVIVALS, -GameRules_scoring_add(it, BR_REVIVALS, 0)); + + RemoveGrapplingHooks(it); + StatusEffects_removeall(it, STATUSEFFECT_REMOVE_CLEAR); + + // isn't there another way to initialize these? + SetResource(it, RES_HEALTH, start_health); + SetResource(it, RES_ARMOR, start_armorvalue); + SetResource(it, RES_SHELLS, 0); + SetResource(it, RES_BULLETS, 0); + SetResource(it, RES_ROCKETS, 0); + SetResource(it, RES_CELLS, 0); + SetResource(it, RES_PLASMA, 0); + SetResource(it, RES_FUEL, 0); + STAT(WEAPONS, it) = '0 0 0'; + it.br_wepset_old = start_weapons; + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + it.br_weapon_prev[slot] = WEP_Null; + it.br_lastweapon_prev[slot] = 0; + } + + ++num_players; + }); + + max_squad_size = max(autocvar_g_br_squad_size, 1); + if(num_players <= max_squad_size) + max_squad_size = ceil(num_players / 2); + + for(int num_squads = 0; (num_squads * max_squad_size) < num_players; ++num_squads) + { + entity new_squad = spawn(); + new_squad.br_squad_drop_leader = NULL; + new_squad.br_squad_id = num_squads + 1; + + IL_PUSH(squads, new_squad); + } + + FOREACH_CLIENT(IS_PLAYER(it), { + entity current_squad = br_SquadGetRandomAvail(); + br_SquadMember_Add(current_squad, it); + GameRules_scoring_add(it, BR_SQUAD, current_squad.br_squad_id); + + setorigin(it, dropship.origin + eZ * (dropship.mins.z - it.maxs.z - 64)); // FIXME: this can teleport players into brushes/void + it.angles = vectoangles(dropship_path_direction) + '45 0 0'; + it.fixangle = true; + it.velocity = '0 0 0'; + set_movetype(it, MOVETYPE_FLY_WORLDONLY); + it.flags &= ~FL_PICKUPITEMS; + it.dphitcontentsmask &= ~DPCONTENTS_BODY; + it.effects |= EF_NODRAW; + Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_BR_DROPSHIP); + STAT(DROP, it) = DROP_TRANSPORT; + UNSET_ONGROUND(it); // otherwise this isn't unset if the player drops in the same frame + }); + + squads_colored = autocvar_g_br_squad_colors; + IL_EACH(squads, true, + { + if(squads_colored) + { + float squad_color; + squad_color = floor(random() * 256); + + for(entity member = it.br_squad_first; member; member = member.br_squad_next) + { + setcolor(member, squad_color); + } + } + + float min_distance = max(autocvar_g_br_drop_distance_force, 0); + if(!br_SquadIsBotsOnly(it)) + it.br_force_drop_distance = min_distance; + else + it.br_force_drop_distance = min_distance + random() * max(dropship_path_length - min_distance, 0); + }); +} + +void br_Initialize() +{ + // TODO: remove round handler + round_handler_Spawn(br_CheckPlayers, br_CheckWinner, br_RoundStart); + round_handler_Init(5, 0, 0); // no warmup or timelimit in battle royale + + EliminatedPlayers_Init(br_isEliminated); +} diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_br.qh b/qcsrc/common/gamemodes/gamemode/br/sv_br.qh new file mode 100644 index 000000000..010bf2753 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_br.qh @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +bool squads_colored = false; + +void br_Initialize(); + +REGISTER_MUTATOR(br, false) +{ + MUTATOR_STATIC(); + MUTATOR_ONADD + { + GameRules_teams(false); + GameRules_limit_score(0); + GameRules_limit_lead(0); + GameRules_score_enabled(false); + GameRules_scoring(0, 0, 0, { + field(SP_BR_RANK, "rank", SFL_LOWER_IS_BETTER | SFL_RANK | SFL_SORT_PRIO_PRIMARY); + field(SP_BR_SQUAD, "squad", 0); + field(SP_BR_REVIVALS, "revivals", 0); + }); + + br_Initialize(); + } + return 0; +} + +float autocvar_g_br_revive_extra_size = 100; +float autocvar_g_br_revive_speed = 0.4; +float autocvar_g_br_revive_clearspeed = 1.6; +float autocvar_g_br_bleed = 0.02; +float autocvar_g_br_bleedlinear = 1; +int autocvar_g_br_squad_size = 3; +int autocvar_g_br_minplayers = 2; diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc b/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc new file mode 100644 index 000000000..60960e54c --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc @@ -0,0 +1,162 @@ +#include "sv_dropship.qh" + +float autocvar_g_br_dropship_scale = 3; +vector autocvar_g_br_dropship_color = '0.5 0 0.5'; + +entity dropship_spawn(Vehicle info, float entity_scale, vector color); +void dropship_think(entity this); +vector dropship_getMultipliers(); +vector dropship_seekPoint(entity this, vector orig, int axis, int direction, float multiplier); + +entity dropship_initialize() +{ + entity this = dropship_spawn(VEH_RACER, autocvar_g_br_dropship_scale, autocvar_g_br_dropship_color); + + bool moveSucceeded = MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 2500, 8192, 1024); + if(!moveSucceeded) + { + delete(this); + return NULL; + } + + vector mult; + + vector startorigin; + startorigin = dropship_seekPoint(this, this.origin, 2, 2, 1); + startorigin = dropship_seekPoint(this, startorigin, 0, 0, 1); + startorigin = dropship_seekPoint(this, startorigin, 1, 0, 1); + mult = dropship_getMultipliers(); + startorigin = dropship_seekPoint(this, startorigin, 0, 1, mult.x); + startorigin = dropship_seekPoint(this, startorigin, 1, 1, mult.y); + + vector endorigin; + mult = dropship_getMultipliers(); + endorigin = dropship_seekPoint(this, startorigin, 0, 1, 1 - mult.x); + endorigin = dropship_seekPoint(this, endorigin, 1, 1, 1 - mult.y); + + endorigin = startorigin + normalize(endorigin - startorigin) * vlen(world.maxs - world.mins); + + tracebox(startorigin, this.mins, this.maxs, endorigin, MOVE_NORMAL, this); + dropship_path_length = trace_fraction * vlen(endorigin - startorigin); + endorigin = trace_endpos; + dropship_path_direction = normalize(endorigin - startorigin); + + setorigin(this, startorigin); + this.angles = vectoangles(dropship_path_direction); + this.velocity = '0 0 0'; + + return this; +} + +entity dropship_spawn(Vehicle info, float entity_scale, vector color) +{ + entity this = new(vehicle); + this.active = ACTIVE_ACTIVE; + + _setmodel(this, info.model); + + this.vehicle_flags |= VHF_ISVEHICLE; + + this.takedamage = DAMAGE_NO; + this.bot_attack = false; + this.iscreature = true; + this.teleportable = false; + this.damagedbycontents = false; + this.vehicleid = info.vehicleid; + this.vehicledef = info; + this.dphitcontentsmask = DPCONTENTS_SOLID; + this.flags = FL_NOTARGET; + this.nextthink = time; + setthink(this, dropship_think); + + this.scale = entity_scale; + setsize(this, info.m_mins * entity_scale, info.m_maxs * entity_scale); + set_movetype(this, MOVETYPE_FLY_WORLDONLY); + + this.colormod = color; + this.alpha = 1; + + CSQCMODEL_AUTOINIT(this); + + return this; +} + +void dropship_think(entity this) +{ + this.nextthink = time; + + if(dropship_path_length > 0){ + this.alpha = bound(0.01, dropship_path_length / autocvar_g_br_drop_distance_force, 1); + this.velocity = dropship_path_direction * autocvar_g_br_dropship_speed; + dropship_path_length -= autocvar_g_br_dropship_speed * frametime; + } + else{ + delete(this); + } + + CSQCMODEL_AUTOUPDATE(this); +} + +vector dropship_getMultipliers() +{ + vector mult; + mult.x = (1 - cos(random() * 90 * DEG2RAD)) * 0.5; + mult.y = min((1 - cos(random() * 90 * DEG2RAD)) * 0.5, 0.5 - mult.x); + mult.z = 0; + bool multswap = (random() >= 0.5); + if(multswap){ + float tmp; + tmp = mult.x; + mult.x = mult.y; + mult.y = tmp; + } + + return mult; +} + +vector dropship_seekPoint(entity this, vector orig, int axis, int direction, float multiplier) +{ + vector vec_axis; + switch(axis) + { + default: case 0: + vec_axis = eX; + break; + case 1: + vec_axis = eY; + break; + case 2: + vec_axis = eZ; + } + + float first_fraction; + float second_fraction = 0; + vector first_end; + vector second_end = '0 0 0'; + + first_end = orig; + first_end = first_end - first_end * vec_axis * vec_axis + world.maxs * vec_axis * vec_axis; + first_fraction = (tracebox(orig, this.mins, this.maxs, first_end, MOVE_NORMAL, this), trace_fraction); + + if(direction != 2) + { + second_end = orig; + second_end = second_end - second_end * vec_axis * vec_axis + world.mins * vec_axis * vec_axis; + second_fraction = (tracebox(orig, this.mins, this.maxs, second_end, MOVE_NORMAL, this), trace_fraction); + } + + float dist_to_edge; + if(((direction == 0) && (first_fraction < second_fraction)) || + ((direction == 1) && (first_fraction > second_fraction)) || + (direction == 2)) + { + dist_to_edge = (first_end * vec_axis - orig * vec_axis) * first_fraction * multiplier; + } + else + { + dist_to_edge = (second_end * vec_axis - orig * vec_axis) * second_fraction * multiplier; + } + orig = orig + dist_to_edge * vec_axis; + + return orig; +} diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qh b/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qh new file mode 100644 index 000000000..4aab1ab96 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qh @@ -0,0 +1,9 @@ +#pragma once + +float dropship_path_length = 0; +vector dropship_path_direction = '0 0 0'; + +float autocvar_g_br_drop_distance_force = 500; +float autocvar_g_br_dropship_speed = 200; + +entity dropship_initialize(); diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_ring.qc b/qcsrc/common/gamemodes/gamemode/br/sv_ring.qc new file mode 100644 index 000000000..69c6c7372 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_ring.qc @@ -0,0 +1,219 @@ +#include "sv_ring.qh" + +.float br_ring_stage; +.float br_ring_stage_strength[BR_RING_STAGE_MAX]; +.float br_ring_timelimit; + +void ring_link(entity this); +bool ring_send(entity this, entity to, float sf); +void ring_think(entity this); +void ring_newStage(entity this); +bool ring_parseTiming(entity this); +void ring_parseStrength(entity this, bool has_invalid); +void ring_alignPosition(entity this); + +float autocvar_g_br_ring_duration = 150; +vector autocvar_g_br_ring_color = '1 0 0'; +float autocvar_g_br_ring_alpha = 0.5; +float autocvar_g_br_ring_radius = -1; // useful for per map settings +string autocvar_g_br_ring_timing = "0.6 0.8 0.9"; +string autocvar_g_br_ring_strength = "2.5 5 10 20 50"; +float autocvar_g_br_ring_wait = 30; +float autocvar_g_br_ring_center_factor = 0.25; + +entity ring_initialize() +{ + entity this = spawn(); + this.netname = BR_RING_NAME; + + setsize(this, '0 0 0', '0 0 0'); + set_movetype(this, MOVETYPE_NOCLIP); + + this.classname = "ring"; + this.br_ring_start = time; + this.br_ring_duration = max(autocvar_g_br_ring_duration, 1); + this.radius = (autocvar_g_br_ring_radius <= 0) ? vlen(vec2(world.maxs - world.mins)) / 2 : autocvar_g_br_ring_radius; + this.br_ring_stage = -1; + this.colormod = autocvar_g_br_ring_color; // TODO: color changing ring + this.alpha = this.br_ring_alpha = bound(0.01, autocvar_g_br_ring_alpha, 1); + this.br_ring_stage_waittime = max(autocvar_g_br_ring_wait, 0); + + bool has_invalid_timings = ring_parseTiming(this); + ring_parseStrength(this, has_invalid_timings); + + this.strength = this.br_ring_stage_strength[0]; + MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 2500, 0, 0); + ring_alignPosition(this); + ring_link(this); + + return this; +} + +void ring_link(entity this) +{ + Net_LinkEntity(this, false, 0, ring_send); + this.nextthink = this.br_ring_start + this.br_ring_stage_waittime; + this.br_ring_timelimit = this.nextthink - game_starttime; + ring_timelimit(this); + setthink(this, ring_think); +} + +bool ring_send(entity this, entity to, float sf) +{ + WriteHeader(MSG_ENTITY, ENT_CLIENT_RING); + WriteByte(MSG_ENTITY, sf); + if(sf & BR_RING_SETUP) + { + WriteVector(MSG_ENTITY, this.origin); + WriteCoord(MSG_ENTITY, this.br_ring_start); + WriteCoord(MSG_ENTITY, this.br_ring_duration); + WriteCoord(MSG_ENTITY, this.radius); + WriteVector(MSG_ENTITY, this.colormod); + WriteCoord(MSG_ENTITY, this.br_ring_alpha); + + WriteByte(MSG_ENTITY, this.br_ring_stage_count); + WriteCoord(MSG_ENTITY, this.br_ring_stage_waittime); + for(int i = 0; i < (this.br_ring_stage_count + 1); ++i) + WriteCoord(MSG_ENTITY, this.br_ring_stage_timing[i]); + } + + if(sf & BR_RING_MOVE) + { + WriteVector(MSG_ENTITY, this.origin); + WriteVector(MSG_ENTITY, this.velocity); + } + + return true; +} + +void ring_think(entity this) +{ + float time_elapsed = time - this.br_ring_start; + + if(time_elapsed >= (this.br_ring_duration + this.br_ring_stage_waittime * this.br_ring_stage_count)) + { + this.velocity = '0 0 0'; + this.SendFlags |= BR_RING_MOVE; // not really necessary but for completeness sake + this.nextthink = 0; // ring reached its final state, no further thinking required + } + else + { + for(int stage = this.br_ring_stage_count - 1; stage >= 0; --stage) + { + float stage_duration_current = this.br_ring_duration * this.br_ring_stage_timing[stage]; + stage_duration_current += this.br_ring_stage_waittime * stage; + + float stage_duration_next = this.br_ring_duration * this.br_ring_stage_timing[stage + 1]; + stage_duration_next += this.br_ring_stage_waittime * (stage + 1); + + if(time_elapsed >= (stage_duration_current + this.br_ring_stage_waittime)) + { + if(stage != this.br_ring_stage) + { + this.br_ring_stage = stage; + ring_newStage(this); + this.SendFlags |= BR_RING_MOVE; + + Send_Notification(NOTIF_ALL, NULL, MSG_CENTER, CENTER_BR_RING_CLOSE, stage + 1); + } + this.nextthink = this.br_ring_start + stage_duration_next; + break; + } + else if(time_elapsed >= stage_duration_current) + { + if(vlen(this.velocity) > 0) + { + this.velocity = '0 0 0'; + this.SendFlags |= BR_RING_MOVE; + } + this.nextthink = this.br_ring_start + stage_duration_current + this.br_ring_stage_waittime; + this.br_ring_timelimit = this.nextthink - game_starttime; + ring_timelimit(this); + break; + } + } + } +} + +void ring_newStage(entity this) +{ + this.strength = this.br_ring_stage_strength[this.br_ring_stage + 1]; + if(this.br_ring_stage > 0) // first stage should center the ring a bit, no moving required + this.velocity = (this.radius / this.br_ring_duration) * normalize(eX * (random() * 2 - 1) + eY * (random() * 2 - 1)); +} + +bool ring_parseTiming(entity this) +{ + int num_timings = tokenize(autocvar_g_br_ring_timing); + if(num_timings > (BR_RING_STAGE_MAX - 2)) + { + LOG_INFO("too many stages defined by g_br_ring_timing"); + num_timings = BR_RING_STAGE_MAX - 2; + } + int invalid_timings = 0; + for(int i = 0; i < num_timings; ++i) + { + float current_timing = stof(argv(i)); + if((current_timing > this.br_ring_stage_timing[i - invalid_timings]) && (current_timing < 1)) + this.br_ring_stage_timing[i + 1 - invalid_timings] = current_timing; + else + { + ++invalid_timings; + LOG_INFO("invalid timing value \"", argv(i), "\" at position ", itos(i + 1), " in g_br_ring_timing was discarded"); + } + } + this.br_ring_stage_count = num_timings + 1 - invalid_timings; + this.br_ring_stage_timing[0] = 0; + this.br_ring_stage_timing[this.br_ring_stage_count] = 1; + + return (invalid_timings > 0); +} + +void ring_parseStrength(entity this, bool has_invalid) +{ + int num_strength = tokenize(autocvar_g_br_ring_strength); + if(!has_invalid) // don't warn about this if we already got errors in the timing list + { + if(num_strength < (this.br_ring_stage_count + 1)) + { + LOG_INFO("not enough strength values in g_br_ring_strength for the defined stages in g_br_ring_timing"); + } + if(num_strength > (this.br_ring_stage_count + 1)) + { + LOG_INFO("too many strength values in g_br_ring_strength for the defined stages in g_br_ring_timing"); + } + } + for(int i = 0; i < (this.br_ring_stage_count + 1); ++i) + { + float current_strength; + float prev_strength = ((i > 0) ? this.br_ring_stage_strength[i - 1] : 1); + if(i < num_strength) + current_strength = stof(argv(i)); + else + current_strength = prev_strength; + + if(current_strength > 0) + this.br_ring_stage_strength[i] = current_strength; + else + { + this.br_ring_stage_strength[i] = prev_strength; + LOG_INFO("invalid strength value \"", argv(i), "\" at position ", itos(i + 1), " in g_br_ring_strength replaced with preceeding value \"", ftos(prev_strength), "\""); + } + } +} + +void ring_timelimit(entity this) +{ + WriteByte(MSG_ALL, 3); // svc_updatestat + WriteByte(MSG_ALL, 236); // STAT_TIMELIMIT + WriteCoord(MSG_ALL, this.br_ring_timelimit / 60); +} + +void ring_alignPosition(entity this) +{ + float f = bound(0, autocvar_g_br_ring_center_factor, 1); + vector ringorigin = world.mins + (world.maxs - world.mins) * ((0.5 - f/2) + random() * f); + ringorigin.z = this.origin.z; + + setorigin(this, ringorigin); +} diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_ring.qh b/qcsrc/common/gamemodes/gamemode/br/sv_ring.qh new file mode 100644 index 000000000..464b1e8d3 --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_ring.qh @@ -0,0 +1,4 @@ +#pragma once + +entity ring_initialize(); +void ring_timelimit(entity this); diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_squad.qc b/qcsrc/common/gamemodes/gamemode/br/sv_squad.qc new file mode 100644 index 000000000..3e18f1aac --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_squad.qc @@ -0,0 +1,160 @@ +#include "sv_squad.qh" + +.int br_squad_members; +.int br_squad_members_alive; + +int br_SquadUpdateInfo() +{ + int alive_players = 0; + int alive_squads = 0; + + IL_EACH(squads, !it.br_squad_dead, + { + it.br_squad_members_alive = 0; + + for(entity member = it.br_squad_first; member; member = member.br_squad_next) + { + if (IS_DEAD(member) || IS_SPEC(member) || IS_OBSERVER(member)) + continue; + ++alive_players; + + if (STAT(BLEEDING, member)) + continue; + ++it.br_squad_members_alive; + } + + if(it.br_squad_members_alive > 0) + ++alive_squads; + }); + + // all members are either dead or bleeding, kill the squad + IL_EACH(squads, !it.br_squad_dead && (it.br_squad_members_alive == 0), + { + it.br_squad_dead = true; + + for(entity member = it.br_squad_first; member; member = member.br_squad_next) + { + Send_Notification(NOTIF_ONE, member, MSG_CENTER, CENTER_BR_DEAD_SQUAD); + GameRules_scoring_add(member, BR_RANK, alive_squads + 1); + + if (IS_DEAD(member) || IS_SPEC(member) || IS_OBSERVER(member)) + continue; + + // kill player + SetResourceExplicit(member, RES_HEALTH, 0); + if(member.event_damage) + member.event_damage(member, member, member, 1, DEATH_ROT.m_id, DMG_NOWEP, member.origin, '0 0 0'); + } + }); + + FOREACH_CLIENT(IS_REAL_CLIENT(it), + { + STAT(SQUADSALIVE, it) = alive_squads; + STAT(PLAYERSALIVE, it) = alive_players; + }); + + eliminatedPlayers.SendFlags |= 1; + + return alive_squads; +} + +void br_SquadMember_Add(entity squad, entity player) +{ + player.br_squad = squad; + ++squad.br_squad_members; + + entity member_prev = squad.br_squad_last; + squad.br_squad_last = player; + player.br_squad_next = NULL; + player.br_squad_prev = member_prev; + if(member_prev) + member_prev.br_squad_next = player; + + if(squad.br_squad_first == NULL) + squad.br_squad_first = player; +} + +void br_SquadMember_Remove(entity player) +{ + entity squad = player.br_squad; + + if(!squad) + return; + + --squad.br_squad_members; + + entity member_next = player.br_squad_next; + entity member_prev = player.br_squad_prev; + if(member_next) + member_next.br_squad_prev = member_prev; + if(member_prev) + member_prev.br_squad_next = member_next; + + if(squad.br_squad_first == player) + squad.br_squad_first = member_next; + if(squad.br_squad_last == player) + squad.br_squad_last = member_prev; + + if(squad.br_squad_drop_leader == player) + squad.br_squad_drop_leader = NULL; // TODO: delegate another drop leader + + if(!squad.br_squad_first) // empty squad + delete(squad); + + player.br_squad = NULL; + player.br_squad_next = NULL; + player.br_squad_prev = NULL; +} + +bool br_SquadIsBotsOnly(entity squad) +{ + for(entity member = squad.br_squad_first; member; member = member.br_squad_next) + { + if(IS_REAL_CLIENT(member)) + return false; + } + + return true; +} + +entity br_SquadGetRandomAvail() +{ + int num_avail = 0; + IL_EACH(squads, it.br_squad_members < max_squad_size, ++num_avail); + + if(num_avail == 0) + return NULL; + + int target_id = floor(random() * num_avail); + + int current_id = 0; + IL_EACH(squads, it.br_squad_members < max_squad_size, + { + if(current_id == target_id) + return it; + + ++current_id; + }); + + return NULL; +} + +entity br_SquadFindLastAlive(entity squad, bool healthy_only) +{ + if(!squad) + return NULL; + + entity last_alive = NULL; + for(entity member = squad.br_squad_first; member; member = member.br_squad_next) + { + if (!IS_DEAD(member) && !IS_SPEC(member) && !IS_OBSERVER(member) && (!STAT(BLEEDING, member) || !healthy_only)) + { + if(last_alive) + return NULL; // more than one squad member is alive + + last_alive = member; + } + } + + return last_alive; +} diff --git a/qcsrc/common/gamemodes/gamemode/br/sv_squad.qh b/qcsrc/common/gamemodes/gamemode/br/sv_squad.qh new file mode 100644 index 000000000..4f8bd40de --- /dev/null +++ b/qcsrc/common/gamemodes/gamemode/br/sv_squad.qh @@ -0,0 +1,30 @@ +#pragma once + +// player squad entity +.entity br_squad; + +#define IN_SQUAD(a) ((a).br_squad != NULL) +#define SAME_SQUAD(a,b) (IN_SQUAD((a)) && ((a).br_squad == (b).br_squad)) +#define DIFF_SQUAD(a,b) (!IN_SQUAD((a)) || ((a).br_squad != (b).br_squad)) + +int max_squad_size; + +// squad drop leader +.entity br_squad_drop_leader; + +// squad stats +.int br_squad_id; +.bool br_squad_dead; + +// squad member list +.entity br_squad_first; +.entity br_squad_last; +.entity br_squad_next; +.entity br_squad_prev; + +int br_SquadUpdateInfo(); +void br_SquadMember_Add(entity squad, entity player); +void br_SquadMember_Remove(entity player); +bool br_SquadIsBotsOnly(entity squad); +entity br_SquadGetRandomAvail(); +entity br_SquadFindLastAlive(entity squad, bool healthy_only); diff --git a/qcsrc/common/mapinfo.qc b/qcsrc/common/mapinfo.qc index c0b67ff4d..035fd0d98 100644 --- a/qcsrc/common/mapinfo.qc +++ b/qcsrc/common/mapinfo.qc @@ -1142,7 +1142,7 @@ int MapInfo_CurrentFeatures() int req = 0; // TODO: find a better way to check if weapons are required on the map if(!(cvar("g_instagib") || cvar("g_overkill") || cvar("g_nix") || cvar("g_weaponarena") || !cvar("g_pickup_items") || !cvar("g_melee_only") - || cvar("g_race") || cvar("g_cts") || cvar("g_nexball") || cvar("g_ca") || cvar("g_freezetag") || cvar("g_lms"))) + || cvar("g_race") || cvar("g_cts") || cvar("g_nexball") || cvar("g_ca") || cvar("g_freezetag") || cvar("g_lms") || cvar("g_br"))) req |= MAPINFO_FEATURE_WEAPONS; return req; } diff --git a/qcsrc/common/mutators/mutator/waypoints/all.inc b/qcsrc/common/mutators/mutator/waypoints/all.inc index daa5af4d5..656273f6a 100644 --- a/qcsrc/common/mutators/mutator/waypoints/all.inc +++ b/qcsrc/common/mutators/mutator/waypoints/all.inc @@ -57,6 +57,11 @@ REGISTER_WAYPOINT(OnsCPAttack, _("Control point"), "", '1 0.5 0', 2); REGISTER_WAYPOINT(OnsGen, _("Generator"), "", '1 0.5 0', 1); REGISTER_WAYPOINT(OnsGenShielded, _("Generator"), "", '1 0.5 0', 1); +const vector WP_BR_BLEEDING_COLOR = '0.8 0.1 0.1'; +const vector WP_BR_REVIVING_COLOR = '0.2 0.8 0.2'; +REGISTER_WAYPOINT(BRBleeding, _("Bleeding!"), "", WP_BR_BLEEDING_COLOR, 1); +REGISTER_WAYPOINT(BRReviving, _("Reviving"), "", WP_BR_REVIVING_COLOR, 1); + REGISTER_WAYPOINT(Weapon, _("Weapon"), "", '0 0 0', 1); REGISTER_WAYPOINT(Monster, _("Monster"), "", '1 0 0', 1); diff --git a/qcsrc/common/notifications/all.inc b/qcsrc/common/notifications/all.inc index f1b2af8e6..eaa4be977 100644 --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@ -299,6 +299,7 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_INFO_NOTIF(DEATH_SELF_FIRE, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 became a bit too crispy%s%s"), _("^BG%s^K1 felt a little hot%s%s")) MSG_INFO_NOTIF(DEATH_SELF_GENERIC, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_selfkill", _("^BG%s^K1 died%s%s"), "") MSG_INFO_NOTIF(DEATH_SELF_LAVA, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_lava", _("^BG%s^K1 turned into hot slag%s%s"), _("^BG%s^K1 found a hot place%s%s")) + MSG_INFO_NOTIF(DEATH_SELF_RING, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 couldn't escape from ^K2the ring^K1%s%s"), "") MSG_INFO_NOTIF(DEATH_SELF_MON_MAGE, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 was exploded by a Mage%s%s"), "") MSG_INFO_NOTIF(DEATH_SELF_MON_SHAMBLER_CLAW, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1's innards became outwards by a Shambler%s%s"), "") MSG_INFO_NOTIF(DEATH_SELF_MON_SHAMBLER_SMASH, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 was smashed by a Shambler%s%s"), "") @@ -391,6 +392,10 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_INFO_NOTIF(LMS_FORFEIT, N_CHATCON, 1, 0, "s1", "", "", _("^BG%s^F3 forfeited"), "") MSG_INFO_NOTIF(LMS_NOLIVES, N_CONSOLE, 1, 0, "s1", "", "", _("^BG%s^F3 has no more lives left"), "") + MSG_INFO_NOTIF(BR_REVIVED, N_CONSOLE, 2, 0, "s1 s2", "", "", _("^BG%s^K3 was revived by ^BG%s"), "") + MSG_INFO_NOTIF(BR_DOWN, N_CONSOLE, 2, 0, "s1 s2", "", "", _("^BG%s^K1 was downed by ^BG%s"), "") + MSG_INFO_NOTIF(BR_DOWN_SELF, N_CONSOLE, 1, 0, "s1", "", "", _("^BG%s^K1 downed themself"), "") + MSG_INFO_NOTIF(MONSTERS_DISABLED, N_CONSOLE, 0, 0, "", "", "", _("^BGMonsters are currently disabled"), "") MULTITEAM_INFO(NEXBALL_RETURN_HELD, N_CONSOLE, 0, 0, "", "", "", _("^BGThe ^TC^TT^BG team held the ball for too long"), "", NAME) @@ -622,6 +627,7 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_CENTER_NOTIF(DEATH_SELF_FIRE, N_ENABLE, 0, 0, "", CPID_Null, "0 0", BOLD(_("^K1You got a little bit too crispy!")), BOLD(_("^K1You felt a little too hot!"))) MSG_CENTER_NOTIF(DEATH_SELF_GENERIC, N_ENABLE, 0, 0, "", CPID_Null, "0 0", BOLD(_("^K1You fragged yourself!")), BOLD(_("^K1You need to be more careful!"))) MSG_CENTER_NOTIF(DEATH_SELF_LAVA, N_ENABLE, 0, 0, "", CPID_Null, "0 0", BOLD(_("^K1You couldn't stand the heat!")), "") + MSG_CENTER_NOTIF(DEATH_SELF_RING, N_ENABLE, 0, 0, "", CPID_Null, "0 0", BOLD(_("^K1You couldn't escape from ^K2the ring^K1!")), "") MSG_CENTER_NOTIF(DEATH_SELF_MONSTER, N_ENABLE, 0, 0, "", CPID_Null, "0 0", BOLD(_("^K1You were killed by a monster!")), BOLD(_("^K1You need to watch out for monsters!"))) MSG_CENTER_NOTIF(DEATH_SELF_NADE, N_ENABLE, 0, 0, "", CPID_Null, "0 0", BOLD(_("^K1You forgot to put the pin back in!")), BOLD(_("^K1Tastes like chicken!"))) MSG_CENTER_NOTIF(DEATH_SELF_NADE_NAPALM, N_ENABLE, 0, 0, "", CPID_Null, "0 0", BOLD(_("^K1Hanging around a napalm explosion is bad!")), "") @@ -704,6 +710,19 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_CENTER_NOTIF(LMS_NOLIVES, N_ENABLE, 0, 0, "", CPID_LMS, "0 0", _("^BGYou have no lives left, you must wait until the next match"), "") + MSG_CENTER_NOTIF(BR_JOIN_LATE, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^BGMatch already started, you must wait until the next match"), "") + MSG_CENTER_NOTIF(BR_JOIN_DEAD, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^BGYou are dead, you must wait until the next match"), "") + MSG_CENTER_NOTIF(BR_FORFEIT, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^BGForfeiting not possible"), "") + MSG_CENTER_NOTIF(BR_DOWN_WAIT, N_ENABLE, 0, 0, "", CPID_BR_DOWN, "-1 0", _("^F1Waiting for revive..."), "") + MSG_CENTER_NOTIF(BR_DEAD_SQUAD, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^K1Your squad has been eliminated"), "") + MSG_CENTER_NOTIF(BR_REVIVE, N_ENABLE, 1, 0, "s1", CPID_Null, "0 0", _("^K3You revived ^BG%s"), "") + MSG_CENTER_NOTIF(BR_REVIVED, N_ENABLE, 1, 0, "s1", CPID_Null, "0 0", _("^K3You were revived by ^BG%s"), "") + MSG_CENTER_NOTIF(BR_DOWN, N_ENABLE, 1, 0, "s1", CPID_Null, "0 0", _("^K1You were downed by ^BG%s"), "") + MSG_CENTER_NOTIF(BR_DOWN_SELF, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^K1You downed yourself"), "") + MSG_CENTER_NOTIF(BR_DROPSHIP, N_ENABLE, 0, 0, "", CPID_BR_DROPSHIP, "-1 0", _("^BG^F1Jump^BG to drop"), "") + MSG_CENTER_NOTIF(BR_RING_WARN, N_ENABLE, 0, 0, "", CPID_Null, "0 0", _("^F4You are outside of ^F2the ring^F4, get back in fast!"), "") + MSG_CENTER_NOTIF(BR_RING_CLOSE, N_ENABLE, 0, 1, "f1", CPID_Null, "0 0", _("^F2Ring^F4 is closing! (strength: ^F1%s^F4)"), "") + MSG_CENTER_NOTIF(MISSING_TEAMS, N_ENABLE, 0, 1, "missing_teams", CPID_MISSING_TEAMS, "-1 0", _("^BGWaiting for players to join...\nNeed active players for: %s"), "") MSG_CENTER_NOTIF(MISSING_PLAYERS, N_ENABLE, 0, 1, "f1", CPID_MISSING_PLAYERS, "-1 0", _("^BGWaiting for %s player(s) to join..."), "") @@ -833,6 +852,7 @@ string multiteam_info_sprintf(string input, string teamname) { return ((input != MSG_MULTI_NOTIF(DEATH_SELF_FIRE, N_ENABLE, NULL, INFO_DEATH_SELF_FIRE, CENTER_DEATH_SELF_FIRE) MSG_MULTI_NOTIF(DEATH_SELF_GENERIC, N_ENABLE, NULL, INFO_DEATH_SELF_GENERIC, CENTER_DEATH_SELF_GENERIC) MSG_MULTI_NOTIF(DEATH_SELF_LAVA, N_ENABLE, NULL, INFO_DEATH_SELF_LAVA, CENTER_DEATH_SELF_LAVA) + MSG_MULTI_NOTIF(DEATH_SELF_RING, N_ENABLE, NULL, INFO_DEATH_SELF_RING, CENTER_DEATH_SELF_RING) MSG_MULTI_NOTIF(DEATH_SELF_MON_MAGE, N_ENABLE, NULL, INFO_DEATH_SELF_MON_MAGE, CENTER_DEATH_SELF_MONSTER) MSG_MULTI_NOTIF(DEATH_SELF_MON_SHAMBLER_CLAW, N_ENABLE, NULL, INFO_DEATH_SELF_MON_SHAMBLER_CLAW, CENTER_DEATH_SELF_MONSTER) MSG_MULTI_NOTIF(DEATH_SELF_MON_SHAMBLER_SMASH, N_ENABLE, NULL, INFO_DEATH_SELF_MON_SHAMBLER_SMASH, CENTER_DEATH_SELF_MONSTER) diff --git a/qcsrc/common/notifications/all.qh b/qcsrc/common/notifications/all.qh index babadf8c8..812608a7b 100644 --- a/qcsrc/common/notifications/all.qh +++ b/qcsrc/common/notifications/all.qh @@ -59,6 +59,8 @@ ENUMCLASS(CPID) CASE(CPID, KEYHUNT) CASE(CPID, KEYHUNT_OTHER) CASE(CPID, LMS) + CASE(CPID, BR_DOWN) + CASE(CPID, BR_DROPSHIP) CASE(CPID, MISSING_TEAMS) CASE(CPID, MISSING_PLAYERS) CASE(CPID, INSTAGIB_FINDAMMO) diff --git a/qcsrc/common/scores.qh b/qcsrc/common/scores.qh index cf50a7fd1..999ea62be 100644 --- a/qcsrc/common/scores.qh +++ b/qcsrc/common/scores.qh @@ -88,6 +88,10 @@ REGISTER_SP(NEXBALL_FAULTS); REGISTER_SP(ONS_TAKES); REGISTER_SP(ONS_CAPS); + +REGISTER_SP(BR_RANK); +REGISTER_SP(BR_SQUAD); +REGISTER_SP(BR_REVIVALS); #endif diff --git a/qcsrc/common/stats.qh b/qcsrc/common/stats.qh index 463d98c85..0659f5b67 100644 --- a/qcsrc/common/stats.qh +++ b/qcsrc/common/stats.qh @@ -126,6 +126,10 @@ REGISTER_STAT(HEALING_ORB_ALPHA, float) REGISTER_STAT(PLASMA, int) REGISTER_STAT(FROZEN, int) REGISTER_STAT(REVIVE_PROGRESS, float) +REGISTER_STAT(BLEEDING, bool) +REGISTER_STAT(DROP, int) +REGISTER_STAT(SQUADSALIVE, int) +REGISTER_STAT(PLAYERSALIVE, int) REGISTER_STAT(ROUNDLOST, int) REGISTER_STAT(CAPTURE_PROGRESS, float) REGISTER_STAT(ENTRAP_ORB, float) diff --git a/qcsrc/lib/csqcmodel/cl_player.qc b/qcsrc/lib/csqcmodel/cl_player.qc index 5b35bfa4e..d1793a0f8 100644 --- a/qcsrc/lib/csqcmodel/cl_player.qc +++ b/qcsrc/lib/csqcmodel/cl_player.qc @@ -613,7 +613,7 @@ void CSQCPlayer_SetCamera() #endif CSQCPlayer_SetMinsMaxs(e); - if (!IS_DEAD(e)) + if (!IS_DEAD(e) && (STAT(DROP) != DROP_FALLING)) e.angles.y = input_angles.y; } diff --git a/qcsrc/menu/xonotic/util.qc b/qcsrc/menu/xonotic/util.qc index e77049d20..227aa8ea8 100644 --- a/qcsrc/menu/xonotic/util.qc +++ b/qcsrc/menu/xonotic/util.qc @@ -683,6 +683,7 @@ float updateCompression() GAMETYPE(MAPINFO_TYPE_ASSAULT) \ /* GAMETYPE(MAPINFO_TYPE_DUEL) */ \ /* GAMETYPE(MAPINFO_TYPE_INVASION) */ \ + /* GAMETYPE(MAPINFO_TYPE_BR) */ \ /**/ // hidden gametypes come last so indexing always works correctly diff --git a/qcsrc/server/command/cmd.qc b/qcsrc/server/command/cmd.qc index a8a8747e1..cefc4d5ac 100644 --- a/qcsrc/server/command/cmd.qc +++ b/qcsrc/server/command/cmd.qc @@ -906,7 +906,7 @@ void SV_ParseClientCommand(entity this, string command) // since gamecode doesn't have any calls earlier than this, do the connecting message here Send_Notification(NOTIF_ALL, NULL, MSG_INFO, INFO_CONNECTING, this.netname); } - if(teamplay) + if(teamplay || (squads_colored && IN_SQUAD(this))) return; break; case "c2s": Net_ClientCommand(this, command); return; // handled by net.qh diff --git a/qcsrc/server/damage.qc b/qcsrc/server/damage.qc index d480c229b..0ccf32bce 100644 --- a/qcsrc/server/damage.qc +++ b/qcsrc/server/damage.qc @@ -305,9 +305,9 @@ void Obituary(entity attacker, entity inflictor, entity targ, int deathtype, .en // ====== // MURDER // ====== - else if(IS_PLAYER(attacker)) + else if(IS_PLAYER(attacker) || IN_SQUAD(attacker)) { - if(SAME_TEAM(attacker, targ)) + if(SAME_TEAM(attacker, targ) || SAME_SQUAD(attacker, targ)) { LogDeath("tk", deathtype, attacker, targ); GiveFrags(attacker, targ, -1, deathtype, weaponentity); @@ -594,7 +594,7 @@ void Damage(entity targ, entity inflictor, entity attacker, float damage, int de // special rule: gravity bombs and sound-based attacks do not affect team mates (other than for disconnecting the hook) if(DEATH_ISWEAPON(deathtype, WEP_HOOK) || (deathtype & HITTYPE_SOUND)) { - if(IS_PLAYER(targ) && SAME_TEAM(targ, attacker)) + if(IS_PLAYER(targ) && (SAME_TEAM(targ, attacker) || SAME_SQUAD(targ, attacker))) { return; } @@ -633,7 +633,7 @@ void Damage(entity targ, entity inflictor, entity attacker, float damage, int de damage = 0; force = '0 0 0'; } - else if(!STAT(FROZEN, targ) && SAME_TEAM(attacker, targ)) + else if(!STAT(FROZEN, targ) && (SAME_TEAM(attacker, targ) || SAME_SQUAD(attacker, targ))) { if(autocvar_teamplay_mode == 1) damage = 0; @@ -781,7 +781,7 @@ void Damage(entity targ, entity inflictor, entity attacker, float damage, int de if(IS_PLAYER(victim) || (IS_TURRET(victim) && victim.active == ACTIVE_ACTIVE) || IS_MONSTER(victim) || MUTATOR_CALLHOOK(PlayHitsound, victim, attacker)) { - if (DIFF_TEAM(victim, attacker)) + if (DIFF_TEAM(victim, attacker) && DIFF_SQUAD(victim, attacker)) { if(damage > 0) { diff --git a/qcsrc/server/world.qc b/qcsrc/server/world.qc index 728c9d757..4fb2b5795 100644 --- a/qcsrc/server/world.qc +++ b/qcsrc/server/world.qc @@ -301,6 +301,7 @@ void cvar_changes_init() BADCVAR("g_tdm"); BADCVAR("g_tdm_on_dm_maps"); BADCVAR("g_tdm_teams"); + BADCVAR("g_br"); BADCVAR("g_vip"); BADCVAR("leadlimit"); BADCVAR("nextmap"); -- 2.39.2