From: Juhu <5894800-Juhu_@users.noreply.gitlab.com> Date: Fri, 1 Jul 2022 02:46:01 +0000 (+0200) Subject: Merge branch 'master' into Juhu/battle-royale X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=37c0c0c8ecf6f7df59379072bf29c4f3f8a6092f;p=xonotic%2Fxonotic-data.pk3dir.git Merge branch 'master' into Juhu/battle-royale --- 37c0c0c8ecf6f7df59379072bf29c4f3f8a6092f diff --cc .gitlab-ci.yml index 48a269313,8b56b81ff..26bdd574e --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@@ -31,9 -54,9 +54,9 @@@ test_sv_game - wget -O data/maps/stormkeep.mapinfo https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.mapinfo - wget -O data/maps/stormkeep.waypoints https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints - wget -O data/maps/stormkeep.waypoints.cache https://gitlab.com/xonotic/xonotic-maps.pk3dir/raw/master/maps/stormkeep.waypoints.cache - - make - - EXPECT=2ed9b21b906b98ba3e9c358b1f41d547 - - HASH=$(${ENGINE} -noconfig -nohome +timestamps 1 +exec serverbench.cfg + - - EXPECT=8e36763a3b4590356bcc449b2452d0ea ++ - EXPECT=79ebc686ac31153de17e6b8b268c3ac2 + - HASH=$(${ENGINE} +timestamps 1 +exec serverbench.cfg | tee /dev/stderr | sed -e 's,^\[[^]]*\] ,,' | grep '^:' diff --cc gamemodes-client.cfg index cf3e35b69,71d272a17..ad35029ec --- a/gamemodes-client.cfg +++ b/gamemodes-client.cfg @@@ -32,7 -32,6 +32,7 @@@ alias cl_hook_gamestart_k 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_gameend alias cl_hook_shutdown alias cl_hook_activeweapon diff --cc qcsrc/common/deathtypes/all.inc index c877f8862,ac42a5bb2..a5b6b2b0b --- a/qcsrc/common/deathtypes/all.inc +++ b/qcsrc/common/deathtypes/all.inc @@@ -10,12 -10,11 +10,12 @@@ REGISTER_DEATHTYPE(GENERIC 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, DEATH_MURDER_RING, "") 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") - REGISTER_DEATHTYPE(MONSTER_SHAMBLER_SMASH, DEATH_SELF_MON_SHAMBLER_SMASH, DEATH_MURDER_MONSTER, "monster") - REGISTER_DEATHTYPE(MONSTER_SHAMBLER_ZAP, DEATH_SELF_MON_SHAMBLER_ZAP, DEATH_MURDER_MONSTER, "monster") + REGISTER_DEATHTYPE(MONSTER_GOLEM_CLAW, DEATH_SELF_MON_GOLEM_CLAW, DEATH_MURDER_MONSTER, "monster") + REGISTER_DEATHTYPE(MONSTER_GOLEM_SMASH, DEATH_SELF_MON_GOLEM_SMASH, DEATH_MURDER_MONSTER, "monster") + REGISTER_DEATHTYPE(MONSTER_GOLEM_ZAP, DEATH_SELF_MON_GOLEM_ZAP, DEATH_MURDER_MONSTER, "monster") REGISTER_DEATHTYPE(MONSTER_SPIDER, DEATH_SELF_MON_SPIDER, DEATH_MURDER_MONSTER, "monster") REGISTER_DEATHTYPE(MONSTER_WYVERN, DEATH_SELF_MON_WYVERN, DEATH_MURDER_MONSTER, "monster") REGISTER_DEATHTYPE(MONSTER_ZOMBIE_JUMP, DEATH_SELF_MON_ZOMBIE_JUMP, DEATH_MURDER_MONSTER, "monster") diff --cc qcsrc/common/ent_cs.qc index b976d651e,536637145..222e74900 --- a/qcsrc/common/ent_cs.qc +++ b/qcsrc/common/ent_cs.qc @@@ -188,8 -188,7 +188,8 @@@ ENTCS_PROP(SOLID, true, sv_solid, solid { if (radar_showenemies) break; if (SAME_TEAM(to, player)) break; + if (SAME_SQUAD(to, player)) break; - if (!(IS_PLAYER(to) || to.caplayer)) break; + if (!(IS_PLAYER(to) || INGAME(to))) break; } sf &= ENTCS_PUBLICMASK; // no private updates } while (0); diff --cc qcsrc/common/gamemodes/gamemode/br/sv_br.qc index 83c2f202f,000000000..dee0dbfeb mode 100644,000000..100644 --- a/qcsrc/common/gamemodes/gamemode/br/sv_br.qc +++ b/qcsrc/common/gamemodes/gamemode/br/sv_br.qc @@@ -1,1221 -1,0 +1,1221 @@@ +// battle royale +// author: Juhu + +#include "sv_br.qh" +#include - #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) \ + || DEATH_ISWEAPON(dt, WEP_VAPORIZER)) + +float br_CalculatePlayerDropAngle(entity this); +void br_LastPlayerForSquad_Notify(entity squad); +void br_RemovePlayer(entity player); +void br_Revive(entity player); +void br_Start(); +bool br_CheckPlayers(); +int br_WinningCondition(); + +entity dropship; + +bool squads_colored = false; + +const float br_drop_time_secs = 1; +const float drop_speed_vertical_max = 0.9; +bool br_started = false; +.bool br_ring_warned; +.float br_drop_time; +.float br_force_drop_distance; +.int br_drop_launch; +.int br_drop_detached; +.bool br_drop_instructions; +.float br_ring_damage_time; + +.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_bleeding_armor = 50; +float autocvar_g_br_drop_damage = 0.5; +float autocvar_g_br_drop_speed_max = 2.5; +float autocvar_g_br_drop_speed_min = 1.25; +float autocvar_g_br_drop_speed_vertical_min = 0.1; +bool autocvar_g_br_squad_colors = true; +float autocvar_g_br_drop_accel_dive = 50; +float autocvar_g_br_drop_accel_turn = 600; +bool autocvar_g_br_startweapons = false; +float autocvar_g_br_squad_waypoint_distance = 1500; + +MUTATOR_HOOKFUNCTION(br, reset_map_global) +{ + dropship_path_length = 0; // this should kill the dropship + dropship_path_direction = '0 0 0'; + + if(ring) + 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)); + + STAT(DROP, it) = DROP_LANDED; + STAT(BLEEDING, it) = false; + + br_RemovePlayer(it); + + 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; + } + }); + + br_SquadUpdateInfo(); + return true; +} + +MUTATOR_HOOKFUNCTION(br, CheckRules_World) +{ + if(!br_started && !warmup_stage && (time > game_starttime) && br_CheckPlayers()) + br_Start(); + + M_ARGV(0, float) = br_WinningCondition(); + return true; +} + +MUTATOR_HOOKFUNCTION(br, GiveFragsForKill, CBC_ORDER_FIRST) +{ + M_ARGV(2, float) = 0; // no frags counted in Battle Royale + 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) +{ + entity player = M_ARGV(0, entity); + + STAT(SQUADCOLORS, player) = squads_colored; + + 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 (!br_started) + { + if(!warmup_stage && (time > game_starttime) && br_CheckPlayers()) + STAT(DROP, player) = DROP_TRANSPORT; // inhibits the spawn effect when the match is about to start + return false; + } + + if (IN_SQUAD(player)) + Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_JOIN_DEAD); + 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); + bool is_forced = M_ARGV(1, bool); + + if(is_forced && IN_SQUAD(player)) + { + br_SquadMember_Remove(player); + br_SquadUpdateInfo(); + } + + if(IN_SQUAD(player)) + { + player.frags = FRAGS_PLAYER_OUT_OF_GAME; + return true; + } + + return false; +} + +MUTATOR_HOOKFUNCTION(br, SpectateCopy) +{ + entity spectatee = M_ARGV(0, entity); + entity client = M_ARGV(1, entity); + + STAT(DROP, client) = STAT(DROP, spectatee); + STAT(BLEEDING, client) = STAT(BLEEDING, spectatee); +} + +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_PLAYER(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_PLAYER(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, ForbidSpawn) +{ + return br_started; +} + +MUTATOR_HOOKFUNCTION(br, WantWeapon) +{ + if(autocvar_g_br_startweapons) + return false; + + M_ARGV(1, float) = 0; + return true; +} + +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_PLAYER(it) && !IS_DEAD(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) + { + const float ring_damage_interval = 0.75; + vector current_origin; + if(!player.vehicle) + current_origin = player.origin + player.view_ofs; + else + current_origin = player.vehicle.origin; + if(vlen(current_origin - ring.origin) > ring_calculate_current_radius(ring)) + { + if(!player.br_ring_warned) + { + player.br_ring_warned = true; + player.br_ring_damage_time = time + ring_damage_interval; + Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_BR_RING_WARN); + } + + // ring damage + if (player.br_ring_damage_time < time) + { + if(player.vehicle) // if the player is controlling a vehicle + { + if(autocvar_g_br_ring_exitvehicle) + vehicles_exit(player.vehicle, VHEF_RELEASE); // begone! + else + vehicles_damage(player.vehicle, ring, ring, 10 * ring.strength * ring_damage_interval, DEATH_RING.m_id, DMG_NOWEP, player.vehicle.origin, '0 0 0'); + } + Damage(player, ring, ring, ring.strength * ring_damage_interval, DEATH_RING.m_id, DMG_NOWEP, player.origin, '0 0 0'); + player.br_ring_damage_time = time + ring_damage_interval; + } + } + else + { + player.br_ring_warned = false; + } + } + + if((STAT(DROP, player) == DROP_FALLING) && (player.br_drop_detached != 2) && IN_SQUAD(player) && (player != player.br_squad.br_squad_drop_leader)){ + // atck2 has to be released then pressed to detach + if(!(STAT(PRESSED_KEYS, player) & KEY_ATCK2)){ + if(player.br_drop_detached == 0){ + player.br_drop_detached = 1; + } + } + else{ + if(player.br_drop_detached == 1){ + player.br_drop_detached = 2; + Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP); + } + } + } + + if(STAT(DROP, player) == DROP_TRANSPORT){ + if(time > (player.br_squad.br_drop_time + br_drop_time_secs)) + { + if(!player.br_drop_instructions) + { + player.br_drop_instructions = true; + Send_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CENTER_BR_DROPSHIP); + } + + // jump has to be released then pressed to launch + if(!(STAT(PRESSED_KEYS, player) & KEY_JUMP)){ + if(player.br_drop_launch == 0){ + player.br_drop_launch = 1; + } + } + else{ + if(player.br_drop_launch == 1){ + player.br_drop_launch = 2; + } + } + } + + if(!(IS_REAL_CLIENT(player) && (player.br_drop_launch == 2)) && (dropship_path_length > player.br_squad.br_force_drop_distance)){ + player.velocity = dropship_path_direction * dropship_speed; + } + else{ + if(!(IN_SQUAD(player) && player.br_squad.br_squad_drop_leader)) + { + player.effects &= ~EF_NODRAW; + player.takedamage = DAMAGE_AIM; + player.solid = SOLID_SLIDEBOX; + if(!autocvar__notarget) + player.flags &= ~FL_NOTARGET; + Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP); + STAT(DROP, player) = DROP_FALLING; + player.br_drop_detached = 0; + float mindropspeed = PHYS_MAXAIRSPEED(player) * max(autocvar_g_br_drop_speed_min, 0); // no maxspeed_mod available here + float maxdropspeed_ratio = drop_speed_vertical_max; // moving straight down is glitchy + float mindropspeed_ratio = bound(0, autocvar_g_br_drop_speed_vertical_min, drop_speed_vertical_max); + float pitch_view = max(player.v_angle.x, 0); + + // pitch_view angle needs to be between 0 and 90 degrees + if(pitch_view > 90) + pitch_view = 180 - pitch_view; + + player.velocity.x = cos(player.angles.y * DEG2RAD); + player.velocity.y = sin(player.angles.y * DEG2RAD); + player.velocity.z = -tan(bound(asin(mindropspeed_ratio), pitch_view * DEG2RAD, asin(maxdropspeed_ratio))); + + player.velocity = normalize(player.velocity) * mindropspeed; + + player.angles.x = br_CalculatePlayerDropAngle(player) - 90; + player.angles.y = vectoangles(vec2(player.velocity)).y + 180; + player.angles.z = 180; + + if(IN_SQUAD(player) && ((IS_REAL_CLIENT(player) && (player.br_drop_launch == 2)) || br_SquadIsBotsOnly(player.br_squad))) + { + player.br_squad.br_squad_drop_leader = player; + + vector drop_base_offset; + drop_base_offset.x = cos((player.angles.y + 90) * DEG2RAD); + drop_base_offset.y = sin((player.angles.y + 90) * DEG2RAD); + drop_base_offset.z = 0; + drop_base_offset = drop_base_offset * vlen(vec2(player.maxs - player.mins)) + drop_base_offset * 32; // I hope individual players never get different mins/maxs + + vector drop_offset = drop_base_offset; + + FOREACH_CLIENT(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (STAT(DROP, it) == DROP_TRANSPORT), { + it.effects &= ~EF_NODRAW; + it.takedamage = DAMAGE_AIM; + it.solid = SOLID_SLIDEBOX; + if(!autocvar__notarget) + it.flags &= ~FL_NOTARGET; + Kill_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CPID_BR_DROP); + STAT(DROP, it) = DROP_FALLING; + it.br_drop_detached = 0; + Send_Notification(NOTIF_ONE_ONLY, it, MSG_CENTER, CENTER_BR_DROP_DETACH); + + setorigin(it, player.origin + drop_offset); // FIXME: this can teleport players into brushes/void + drop_offset += drop_base_offset; + + it.velocity = player.velocity; + it.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; + + // set the drop stat to landed on the next frame if it was set on landing + if(STAT(DROP, player) == DROP_LANDING) + STAT(DROP, player) = DROP_LANDED; + + // TODO: improve dropping physics + if(STAT(DROP, player) == DROP_FALLING){ + if(!IS_ONGROUND(player) && (player.waterlevel < WATERLEVEL_SWIMMING) && ((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_drop_detached != 2) && (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 maxairspeed = PHYS_MAXAIRSPEED(player) * max(maxspeed_mod, 1); + float maxdropspeed = maxairspeed * max(autocvar_g_br_drop_speed_max, 0); + float mindropspeed = maxairspeed * max(autocvar_g_br_drop_speed_min, 0); + float maxdropspeed_ratio = drop_speed_vertical_max; // moving straight down is glitchy + float mindropspeed_ratio = bound(0, autocvar_g_br_drop_speed_vertical_min, drop_speed_vertical_max); + float accel_dive = max(autocvar_g_br_drop_accel_dive, 0); + float accel_turn = max(autocvar_g_br_drop_accel_turn, 0); + float dropspeed = vlen(player.velocity); + float dropspeed_xy = vlen(vec2(player.velocity)); + float pitch_current = br_CalculatePlayerDropAngle(player); + float pitch_view = max(player.v_angle.x, 0); + + // pitch_view angle needs to be between 0 and 90 degrees + if(pitch_view > 90) + pitch_view = 180 - pitch_view; + + float pitch_diff = pitch_current - pitch_view; + float pitch_ratio_wish = 0; + + // calculate how much the player wants to change pitch (ratio is at least 0.1) + // ratio is between -1 (looking straight down) and +1 (looking straight ahead or up) + if((pitch_diff < 0) && (pitch_current < 90)) + pitch_ratio_wish = bound(-1, sin(pitch_diff / (90 - pitch_current) * M_PI_2), -0.1); + else if((pitch_diff > 0) && (pitch_current > 0)) + pitch_ratio_wish = bound(0.1, sin(pitch_diff / pitch_current * M_PI_2), 1); + + makevectors(player.v_angle); + // horizontal wishvel as usual + vector wishvel = v_forward * CS(player).movement.x + v_right * CS(player).movement.y; + wishvel = normalize(wishvel) * min(1, vlen(wishvel) / maxairspeed); + // vertical wishvel using forward movement and the previously calculated ratio + wishvel.z = pitch_ratio_wish * bound(0, CS(player).movement.x / maxairspeed, 1); + // apply turn acceleration to wishvel + wishvel.x *= accel_turn; + wishvel.y *= accel_turn; + wishvel.z *= accel_turn; + player.velocity += wishvel * dt; + player.velocity = normalize(eZ * player.velocity.z + normalize(vec2(player.velocity)) * dropspeed_xy); + + // if there is no horizontal movement point the horizontal vector towards the view direction + if(vlen(vec2(player.velocity)) == 0) + player.velocity += (eX * cos(player.angles.y * DEG2RAD) + eY * sin(player.angles.y * DEG2RAD)) * sqrt(1 - pow(maxdropspeed_ratio, 2)); + + // modify mindropspeed_ratio and maxdropspeed_ratio so that the player does not rotate beyond the view angle + float pitch_ratio_view = sin(pitch_view * DEG2RAD); + if(pitch_ratio_wish > 0) + mindropspeed_ratio = bound(mindropspeed_ratio, pitch_ratio_view, maxdropspeed_ratio); + else if(pitch_ratio_wish < 0) + maxdropspeed_ratio = bound(mindropspeed_ratio, pitch_ratio_view, maxdropspeed_ratio); + + // constrain to vertical min/maxdropspeed + if(player.velocity.z > -mindropspeed_ratio) + player.velocity.z = -mindropspeed_ratio; + if(player.velocity.z < -maxdropspeed_ratio) + player.velocity.z = -maxdropspeed_ratio; + + // adjust horizontal speed so that vertical speed + horizontal speed = maxdropspeed + float dropangle = br_CalculatePlayerDropAngle(player); + const float accelangle = 20; + dropspeed = bound(mindropspeed, dropspeed + accel_dive * (dropangle - accelangle) / accelangle * dt, maxdropspeed); + player.velocity = normalize(player.velocity) * dropspeed; + + player.angles.x = dropangle - 90; + player.angles.y = vectoangles(vec2(player.velocity)).y + 180; + player.angles.z = 180; + + if(player_is_drop_leader) + { + FOREACH_CLIENT(IS_PLAYER(it) && (it != player) && SAME_SQUAD(it, player) && (it.br_drop_detached != 2) && (STAT(DROP, it) == DROP_FALLING), { + it.velocity = player.velocity; + it.angles = player.angles; + }); + } + else if((player.br_drop_detached != 2) && IN_SQUAD(player)) + { + player.br_drop_detached = 2; + Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP); + } + } + else + { + player.velocity = player.br_squad.br_squad_drop_leader.velocity; + player.angles = player.br_squad.br_squad_drop_leader.angles; // no fixangles, only moves the player model not the player view + } + + return true; + } + else + { + if((player.br_drop_detached != 2) && IN_SQUAD(player) && (player != player.br_squad.br_squad_drop_leader)) + { + player.br_drop_detached = 2; + Kill_Notification(NOTIF_ONE_ONLY, player, MSG_CENTER, CPID_BR_DROP); + } + + STAT(DROP, player) = DROP_LANDING; + set_movetype(player, MOVETYPE_WALK); + ITEMS_STAT(player) &= ~IT_USING_JETPACK; + player.flags |= FL_PICKUPITEMS; + player.dphitcontentsmask |= DPCONTENTS_BODY; + + STAT(WEAPONS, player) = player.br_wepset_old; + + .entity weaponentity = weaponentities[0]; + W_SwitchWeapon_Force(player, w_getbestweapon(player, weaponentity), weaponentity); + } + } + + // injured players can't swim + if(STAT(BLEEDING, player)){ + if(player.waterlevel >= WATERLEVEL_SWIMMING) + { + CS(player).movement.z = -60; // drift towards bottom + player.v_angle.x = 0; + player.com_in_jump = false; + } + } +} + +MUTATOR_HOOKFUNCTION(br, Damage_Calculate) +{ + entity frag_target = M_ARGV(2, entity); + float frag_deathtype = M_ARGV(3, float); + + if(STAT(DROP, frag_target) != DROP_LANDED) + { + // weapon impact has no push force while dropping + M_ARGV(6, vector) = '0 0 0'; + + if(STAT(DROP, frag_target) == DROP_TRANSPORT) + M_ARGV(4, float) = M_ARGV(5, float) = 0; // can't take damage while on the dropship + else if(DEATH_ISWEAPON(frag_deathtype, WEP_VAPORIZER)); // do not adjust vaporizer damage + else + { + switch(frag_deathtype) + { + case DEATH_FALL.m_id: + case DEATH_SHOOTING_STAR.m_id: + // do not take fall damage when landing from dropship + M_ARGV(4, float) = M_ARGV(5, float) = 0; + break; + default: + // only take half of the usual damage + M_ARGV(4, float) *= max(autocvar_g_br_drop_damage, 0); + M_ARGV(5, float) *= max(autocvar_g_br_drop_damage, 0); + } + } + } +} + +MUTATOR_HOOKFUNCTION(br, PlayerDies, CBC_ORDER_FIRST) +{ + 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 true; + + if(STAT(DROP, frag_target) == DROP_TRANSPORT) + { + frag_target.effects &= ~EF_NODRAW; + frag_target.takedamage = DAMAGE_AIM; + frag_target.solid = SOLID_SLIDEBOX; + if(!autocvar__notarget) + frag_target.flags &= ~FL_NOTARGET; + Kill_Notification(NOTIF_ONE_ONLY, frag_target, MSG_CENTER, CPID_BR_DROP); + } + + if(STAT(DROP, frag_target) == DROP_FALLING) + { + if((frag_target.br_drop_detached != 2) && IN_SQUAD(frag_target) && (frag_target != frag_target.br_squad.br_squad_drop_leader)) + { + frag_target.br_drop_detached = 2; + Kill_Notification(NOTIF_ONE_ONLY, frag_target, MSG_CENTER, CPID_BR_DROP); + } + } + + if(STAT(DROP, frag_target) != DROP_LANDED) + { + set_movetype(frag_target, MOVETYPE_WALK); + frag_target.dphitcontentsmask |= DPCONTENTS_BODY; + STAT(WEAPONS, frag_target) = frag_target.br_wepset_old; + 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; + + // restore weapons on death to make weapon drop work + STAT(WEAPONS, frag_target) = frag_target.br_wepset_old; + for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot) + { + .entity weaponentity = weaponentities[slot]; + frag_target.(weaponentity).m_weapon = frag_target.br_weapon_prev[slot]; + } + } + WaypointSprite_Kill(frag_target.br_allywaypoint); + + 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, max(autocvar_g_br_bleeding_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); + + 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(br_started) + { + br_LastPlayerForSquad_Notify(player.br_squad); + br_SquadUpdateInfo(); + } +} + +MUTATOR_HOOKFUNCTION(br, ClientObituary) +{ + entity frag_inflictor = M_ARGV(0, entity); + 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 + //entity frag_weaponentity = M_ARGV(4, entity); + + if(!STAT(BLEEDING, frag_target) && !BR_KILLS_INSTANTLY(frag_target, frag_deathtype)) + { + frag_target.br_bleeding_inflictor = frag_inflictor; + frag_target.br_bleeding_attacker = frag_attacker; + frag_target.br_bleeding_deathtype = frag_deathtype; + //frag_target.br_bleeding_weaponentity = frag_weaponentity; // TODO: get entity field + return true; + } + + if(STAT(BLEEDING, frag_target) && frag_target.br_bleeding_attacker) + { + entity new_inflictor = frag_target.br_bleeding_inflictor; + entity new_attacker = frag_target.br_bleeding_attacker; + int new_deathtype = frag_target.br_bleeding_deathtype; + .entity new_weaponentity = frag_target.br_bleeding_weaponentity; + frag_target.br_bleeding_attacker = frag_target.br_bleeding_inflictor = NULL; + + Obituary(new_attacker, new_inflictor, frag_target, new_deathtype, new_weaponentity); + return true; + } + + return false; +} + +MUTATOR_HOOKFUNCTION(br, SetResource) +{ - entity player = M_ARGV(0, entity); ++ entity player = M_ARGV(7, entity); + if(!IS_PLAYER(player)) + return false; + - int res_type = M_ARGV(1, int); - float amount = M_ARGV(2, float); ++ entity res_type = M_ARGV(8, entity); ++ float amount = M_ARGV(9, 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); ++ entity player = M_ARGV(7, entity); + + if(!IS_PLAYER(player) || !STAT(BLEEDING, player)) + return false; + - int res_type = M_ARGV(1, int); ++ entity res_type = M_ARGV(8, entity); + + switch(res_type) + { + case RES_HEALTH: - M_ARGV(2, float) *= max(autocvar_g_br_bleeding_health, 0); ++ M_ARGV(9, float) *= max(autocvar_g_br_bleeding_health, 0); + break; + case RES_ARMOR: - M_ARGV(2, float) = max(autocvar_g_br_bleeding_armor, 0); ++ M_ARGV(9, float) = max(autocvar_g_br_bleeding_armor, 0); + } +} + +MUTATOR_HOOKFUNCTION(br, PlayerRegen, CBC_ORDER_FIRST) +{ + 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; + } + else{ + M_ARGV(2, float) = M_ARGV(3, float) = 0; // no regeneration or rot in battle royale + } +} + +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, TurretValidateTarget) +{ + entity turret = M_ARGV(0, entity); + entity target = M_ARGV(1, entity); + + if(!br_started || SAME_SQUAD(turret, target) || (STAT(DROP, target) == DROP_TRANSPORT)) + { + M_ARGV(3, float) = -1; + return true; + } + + return false; +} + +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(wp.owner == NULL) + return true; + + if((wp == wp.owner.br_allywaypoint) && (vdist(wp.owner.origin - player.origin, <, autocvar_g_br_squad_waypoint_distance) || STAT(BLEEDING, wp.owner))) + return true; + + if(!IS_PLAYER(player) || DIFF_SQUAD(wp.owner, player)) + return true; +} + +MUTATOR_HOOKFUNCTION(br, ClientKill) +{ + entity player = M_ARGV(0, entity); + + if(br_started) + { + // 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(br_started) + { + // 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; +} + +float br_CalculatePlayerDropAngle(entity this) +{ + if(this.velocity.z < 0) + { + float dropspeed_xy = vlen(vec2(this.velocity)); + float dropspeed_z = fabs(this.velocity.z); + return 90 - atan(dropspeed_xy / dropspeed_z) * RAD2DEG; + } + + return 0; +} + +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)); + SetResource(player, RES_ARMOR, 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(); +} + +int br_WinningCondition() +{ + int total_squads = br_SquadUpdateInfo(); + + if ((total_squads > 1) || !br_started) + 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); + } + + delete(round_handler); + round_handler = NULL; + + return WINNING_YES; +} + +bool br_isEliminated(entity e) +{ + return (IN_SQUAD(e) && (IS_DEAD(e) || !IS_PLAYER(e))); +} + +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_Start(){ + // battle royale does not need those, besides, the timelimit won't be visible anymore after the game started + cvar_set("timelimit", "0"); + cvar_set("fraglimit", "0"); + cvar_set("leadlimit", "0"); + - reset_map(true); ++ reset_map(true, false); + + ring = ring_initialize(); + + dropship = dropship_initialize(); + + if(!dropship) + { + br_started = true; + + 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), { + STAT(DROP, it) = DROP_TRANSPORT; + PutPlayerInServer(it); + + it.br_wepset_old = STAT(WEAPONS, it); + STAT(WEAPONS, it) = '0 0 0'; + 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 = new_pure(squad); + 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); + it.flags &= ~FL_PICKUPITEMS; + it.flags |= FL_NOTARGET; + it.dphitcontentsmask &= ~DPCONTENTS_BODY; + it.effects |= EF_NODRAW; + it.takedamage = DAMAGE_NO; + it.solid = SOLID_NOT; + it.br_drop_instructions = false; + it.br_drop_launch = 0; + UNSET_ONGROUND(it); // otherwise this isn't unset if the player drops in the same frame + + WaypointSprite_Spawn(WP_BRAlly, 0, 0, it, '0 0 64', NULL, 0, it, br_allywaypoint, true, RADARICON_WAYPOINT); + }); + + squads_colored = autocvar_g_br_squad_colors; + + FOREACH_CLIENT(IS_REAL_CLIENT(it), + { + STAT(SQUADCOLORS, it) = squads_colored; + }); + + IL_EACH(squads, true, + { + if(squads_colored) + { + float squad_color; + squad_color = 16 * floor(random() * 15) + floor(random() * 15); // color 15 is special, don't select it as a squad color + + for(entity member = it.br_squad_first; member; member = member.br_squad_next) + { + member.colormap = 1024 + squad_color; + } + } + + it.br_drop_time = time; + + 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 + dropship_speed * br_drop_time_secs), 0); + }); + + round_handler.cnt = 0; // emulate round handler round start + br_started = true; +} + +void br_dummy_Think(entity this){} + +void br_Initialize() +{ + br_started = false; + squads_colored = autocvar_g_br_squad_colors; + + // emulate the round handler, useful because this will cause a lot of code to correctly treat the stage before the match starts + round_handler = new_pure(round_handler); + round_handler.count = 0; + round_handler.cnt = 1; + round_handler.wait = false; + setthink(round_handler, br_dummy_Think); + + EliminatedPlayers_Init(br_isEliminated); +} diff --cc qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc index 767b55121,000000000..01ce3bd8e mode 100644,000000..100644 --- a/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc +++ b/qcsrc/common/gamemodes/gamemode/br/sv_dropship.qc @@@ -1,174 -1,0 +1,174 @@@ +#include "sv_dropship.qh" + +float autocvar_g_br_dropship_scale = 3; +vector autocvar_g_br_dropship_color = '0.5 0 0.5'; +float autocvar_g_br_dropship_speed = -1; + +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); + + for(int i = 0; i < 100; ++i) // try to find a dropship path multiple times + { - if(!MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, this.dphitcontentsmask, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 100, 8192, 1024)) ++ if(!MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, this.dphitcontentsmask, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 100, 8192, 1024, false)) + continue; + + 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(vec2(world.maxs - world.mins)); + + tracebox(startorigin, this.mins, this.maxs, endorigin, MOVE_NORMAL, this); + dropship_path_length = trace_fraction * vlen(endorigin - startorigin); + if(dropship_path_length < (vlen(vec2(world.maxs - world.mins)) / 4)) // if the dropship path isn't atleast one quarter of the diagonal length of the map, retry, we're probably in a building + continue; + endorigin = trace_endpos; + dropship_path_direction = normalize(endorigin - startorigin); + + setorigin(this, startorigin); + this.angles = vectoangles(dropship_path_direction); + this.velocity = '0 0 0'; + + dropship_speed = autocvar_g_br_dropship_speed; + // if dropship_speed is negative adjust speed dependant on map size + if(dropship_speed < 0) + dropship_speed = vlen(vec2(world.maxs - world.mins)) / 60; // dropship needs one minute to diagonally fly over the whole map + + return this; + } + + delete(this); + return NULL; +} + +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; + if(autocvar_g_playerclip_collisions) + this.dphitcontentsmask |= DPCONTENTS_PLAYERCLIP; + 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 * dropship_speed; + dropship_path_length -= 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 --cc qcsrc/common/gamemodes/gamemode/br/sv_ring.qc index 815b26335,000000000..2814c454a mode 100644,000000..100644 --- a/qcsrc/common/gamemodes/gamemode/br/sv_ring.qc +++ b/qcsrc/common/gamemodes/gamemode/br/sv_ring.qc @@@ -1,225 -1,0 +1,225 @@@ +#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'; // FIXME: put visual-only cvar client side? +float autocvar_g_br_ring_alpha = 0.5; // FIXME: put visual-only cvar client side? +float autocvar_g_br_ring_fadedistance = 0.5; // FIXME: put visual-only cvar client side? +float autocvar_g_br_ring_fadedistance_min = 2000; // FIXME: put visual-only cvar client side? +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_fadedistance = max(autocvar_g_br_ring_fadedistance, 0); + this.br_ring_fadedistance_min = max(autocvar_g_br_ring_fadedistance_min, 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_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 2500, 0, 0); ++ MoveToRandomLocationWithinBounds(this, world.mins, world.maxs, DPCONTENTS_SOLID, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 2500, 0, 0, false); + 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); + WriteCoord(MSG_ENTITY, this.br_ring_fadedistance); + WriteCoord(MSG_ENTITY, this.br_ring_fadedistance_min); + + 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 --cc qcsrc/common/notifications/all.inc index 6cf30547a,87398b11d..2ba2a86ad --- a/qcsrc/common/notifications/all.inc +++ b/qcsrc/common/notifications/all.inc @@@ -300,11 -299,10 +300,11 @@@ string multiteam_info_sprintf(string in 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"), "") - MSG_INFO_NOTIF(DEATH_SELF_MON_SHAMBLER_ZAP, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 was zapped to death by a Shambler%s%s"), "") + MSG_INFO_NOTIF(DEATH_SELF_MON_GOLEM_CLAW, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1's innards became outwards by a Golem%s%s"), "") + MSG_INFO_NOTIF(DEATH_SELF_MON_GOLEM_SMASH, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 was smashed by a Golem%s%s"), "") + MSG_INFO_NOTIF(DEATH_SELF_MON_GOLEM_ZAP, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 was zapped to death by a Golem%s%s"), "") MSG_INFO_NOTIF(DEATH_SELF_MON_SPIDER, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 was bitten by a Spider%s%s"), "") MSG_INFO_NOTIF(DEATH_SELF_MON_WYVERN, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 was fireballed by a Wyvern%s%s"), "") MSG_INFO_NOTIF(DEATH_SELF_MON_ZOMBIE_JUMP, N_CONSOLE, 2, 1, "s1 s2loc spree_lost", "s1", "notify_death", _("^BG%s^K1 joins the Zombies%s%s"), "") @@@ -710,21 -703,8 +710,22 @@@ MULTITEAM_CENTER(KEYHUNT_START, N_ENABLE, 0, 0, "", CPID_KEYHUNT, "0 0", _("^BGYou are starting with the ^TC^TT Key"), "", KEY) 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(LMS_SPECWARN, N_ENABLE, 0, 0, "", CPID_LMS, "0 0", _("^F4WARNING:^BG you can't rejoin this match after spectating.\nUse the same command again to spectate anyway."), "") + 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_DROP, "-1 0", _("^BG^F1Jump^BG to drop"), "") + MSG_CENTER_NOTIF(BR_DROP_DETACH, N_ENABLE, 0, 0, "", CPID_BR_DROP, "-1 0", _("^BG^F1Secondary fire^BG to detach"), "") + 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..."), "") @@@ -855,11 -833,10 +855,11 @@@ 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) - MSG_MULTI_NOTIF(DEATH_SELF_MON_SHAMBLER_ZAP, N_ENABLE, NULL, INFO_DEATH_SELF_MON_SHAMBLER_ZAP, CENTER_DEATH_SELF_MONSTER) + MSG_MULTI_NOTIF(DEATH_SELF_MON_GOLEM_CLAW, N_ENABLE, NULL, INFO_DEATH_SELF_MON_GOLEM_CLAW, CENTER_DEATH_SELF_MONSTER) + MSG_MULTI_NOTIF(DEATH_SELF_MON_GOLEM_SMASH, N_ENABLE, NULL, INFO_DEATH_SELF_MON_GOLEM_SMASH, CENTER_DEATH_SELF_MONSTER) + MSG_MULTI_NOTIF(DEATH_SELF_MON_GOLEM_ZAP, N_ENABLE, NULL, INFO_DEATH_SELF_MON_GOLEM_ZAP, CENTER_DEATH_SELF_MONSTER) MSG_MULTI_NOTIF(DEATH_SELF_MON_SPIDER, N_ENABLE, NULL, INFO_DEATH_SELF_MON_SPIDER, CENTER_DEATH_SELF_MONSTER) MSG_MULTI_NOTIF(DEATH_SELF_MON_WYVERN, N_ENABLE, NULL, INFO_DEATH_SELF_MON_WYVERN, CENTER_DEATH_SELF_MONSTER) MSG_MULTI_NOTIF(DEATH_SELF_MON_ZOMBIE_JUMP, N_ENABLE, NULL, INFO_DEATH_SELF_MON_ZOMBIE_JUMP, CENTER_DEATH_SELF_MONSTER) diff --cc qcsrc/menu/xonotic/util.qc index 227aa8ea8,f98a8de88..9c9ed148e --- a/qcsrc/menu/xonotic/util.qc +++ b/qcsrc/menu/xonotic/util.qc @@@ -682,8 -669,6 +669,7 @@@ float updateCompression( GAMETYPE(MAPINFO_TYPE_ONSLAUGHT) \ 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 --cc qcsrc/server/chat.qc index da75146ba,a73de2a1f..37cbcdee2 --- a/qcsrc/server/chat.qc +++ b/qcsrc/server/chat.qc @@@ -31,7 -31,7 +31,7 @@@ int Say(entity source, int teamsay, ent msgin = formatmessage(source, msgin); string colorstr; - if (!(IS_PLAYER(source) || source.caplayer || IN_SQUAD(source))) - if (!(IS_PLAYER(source) || INGAME(source))) ++ if (!(IS_PLAYER(source) || INGAME(source) || IN_SQUAD(source))) colorstr = "^0"; // black for spectators else if(teamplay) colorstr = Team_ColorCode(source.team); @@@ -226,7 -224,7 +226,7 @@@ sourcecmsgstr = cmsgstr; } - if (!privatesay && source && !(IS_PLAYER(source) || source.caplayer || IN_SQUAD(source)) && !game_stopped - if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped ++ if (!privatesay && source && !(IS_PLAYER(source) || INGAME(source) || IN_SQUAD(source)) && !game_stopped && (teamsay || CHAT_NOSPECTATORS())) { teamsay = -1; // spectators @@@ -260,8 -258,8 +260,8 @@@ ret = 1; } - if (privatesay && source && !(IS_PLAYER(source) || source.caplayer || IN_SQUAD(source)) && !game_stopped - && (IS_PLAYER(privatesay) || privatesay.caplayer || IN_SQUAD(privatesay)) && CHAT_NOSPECTATORS()) - if (privatesay && source && !(IS_PLAYER(source) || INGAME(source)) && !game_stopped - && (IS_PLAYER(privatesay) || INGAME(privatesay)) && CHAT_NOSPECTATORS()) ++ if (privatesay && source && !(IS_PLAYER(source) || INGAME(source) || IN_SQUAD(source)) && !game_stopped ++ && (IS_PLAYER(privatesay) || INGAME(privatesay) || IN_SQUAD(privatesay)) && CHAT_NOSPECTATORS()) { ret = -1; // just hide the message completely } @@@ -306,7 -304,7 +306,7 @@@ dedicated_print(msgstr); // send to server console too if(sourcecmsgstr != "") centerprint(source, sourcecmsgstr); - FOREACH_CLIENT((IS_PLAYER(it) || it.caplayer || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && (((it.team == source.team) && !IN_SQUAD(source)) || SAME_SQUAD(it, source)) && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), { - FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && it.team == source.team && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), { ++ FOREACH_CLIENT((IS_PLAYER(it) || INGAME(it) || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && (((it.team == source.team) && !IN_SQUAD(source)) || SAME_SQUAD(it, source)) && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), { sprint(it, msgstr); if(cmsgstr != "") centerprint(it, cmsgstr); @@@ -317,7 -315,7 +317,7 @@@ { sprint(source, sourcemsgstr); dedicated_print(msgstr); // send to server console too - FOREACH_CLIENT(!(IS_PLAYER(it) || it.caplayer || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), { - FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), { ++ FOREACH_CLIENT(!(IS_PLAYER(it) || INGAME(it) || IN_SQUAD(it)) && IS_REAL_CLIENT(it) && it != source && !MUTATOR_CALLHOOK(ChatMessageTo, it, source), { sprint(it, msgstr); }); event_log_msg = sprintf(":chat_spec:%d:%s", source.playerid, strreplace("\n", " ", msgin)); diff --cc qcsrc/server/client.qc index 24f9ef00d,2e6e9e6b8..358b3de4d --- a/qcsrc/server/client.qc +++ b/qcsrc/server/client.qc @@@ -234,12 -235,11 +235,13 @@@ void setplayermodel(entity e, string mo UpdatePlayerSounds(e); } +bool SpectateNext(entity this); + /** putting a client as observer in the server */ - void PutObserverInServer(entity this, bool is_forced) + void PutObserverInServer(entity this, bool is_forced, bool use_spawnpoint) { bool mutator_returnvalue = MUTATOR_CALLHOOK(MakePlayerObserver, this, is_forced); + bool recount_ready = false; PlayerState_detach(this); if (IS_PLAYER(this))