diff --git a/include/g_local.h b/include/g_local.h index f4398805..ff884639 100644 --- a/include/g_local.h +++ b/include/g_local.h @@ -894,6 +894,7 @@ void CA_player_pre_think(void); void CA_spectator_think(void); void CA_Frame(void); void CA_PutClientInServer(void); +void CA_AddLatePlayer(gedict_t *p, char *team); qbool CA_can_fire(gedict_t *p); // captain.c diff --git a/include/progs.h b/include/progs.h index e05dd3c5..3337ffe1 100644 --- a/include/progs.h +++ b/include/progs.h @@ -219,7 +219,8 @@ typedef enum etCaptain, etCoach, etAdmin, - etSuggestColor + etSuggestColor, + etLateJoin } electType_t; // store player votes here @@ -1062,10 +1063,12 @@ typedef struct gedict_s qbool ca_alive; qbool ca_ready; qbool can_respawn; + qbool is_solo; // is player a one-man team? qbool in_play; // is player still fighting? qbool in_limbo; // waiting to respawn during wipeout qbool last_alive_active; // if last alive timer is active qbool no_pain; // if player can take any damage to health or armor + qbool lj_accepted; // if late-join request was accepted float ca_round_frags; float ca_round_kills; float ca_round_dmg; @@ -1077,12 +1080,13 @@ typedef struct gedict_s float ca_round_rldirect; float ca_round_lghit; float ca_round_lgfired; - float alive_time; // number of seconds player is in play + float regen_timer; // when the regen timer is started float time_of_respawn; // server time player respawned or round started float seconds_to_respawn; // number of seconds until respawn float escape_time; // number of seconds after "escaping" char *teamcolor; // color of player's team - char cptext[100]; // centerprint for player + char cptext[1024]; // centerprint for player + char ljteam[1024]; // team that player is requesting to join int ca_ammo_grenades; // grenade ammo int tracking_enabled; int round_deaths; // number of times player has died in the round diff --git a/src/clan_arena.c b/src/clan_arena.c index 873b8eeb..4928fbc1 100644 --- a/src/clan_arena.c +++ b/src/clan_arena.c @@ -4,6 +4,36 @@ #include "g_local.h" +typedef struct wipeout_spawn_config_t +{ + vec3_t origin; // spawn point coordinates + char *name; // spawn point name (for debugging) + float custom_radius; // custom radius for this spawn (0 = use default) +} wipeout_spawn_config; + +typedef struct wipeout_map_spawns_t +{ + char *mapname; + wipeout_spawn_config *spawns; + int spawn_count; +} wipeout_map_spawns; + +// Some spawns require a custom radius to prevent abuse +// Using 0 defaults to a radius of 84 units +static wipeout_spawn_config dm3_spawns[] = { + { { -880, -232, -16 }, "tele/sng", 128 }, + { { 192, -208, -176 }, "big>ra", 0 }, + { { 1472, -928, -24 }, "ya box", 0 }, + { { 1520, 432, -88 }, "rl", 300 }, + { { -632, -680, -16 }, "tele/ra", 128 }, + { { 512, 768, 216 }, "lifts", 128 } +}; + +static wipeout_map_spawns wipeout_spawn_configs[] = { + { "dm3", dm3_spawns, sizeof(dm3_spawns) / sizeof(dm3_spawns[0]) }, + { NULL, NULL, 0 } // terminator +}; + static int round_num; static int team1_score; static int team2_score; @@ -23,9 +53,15 @@ void CA_TeamsStats(void); void CA_SendTeamInfo(gedict_t *t); void print_player_stats(qbool series_over); void CA_OnePlayerStats(gedict_t *p, qbool series_over); +void CA_AddLatePlayer(gedict_t *p, char *team); void EndRound(int alive_team); void show_tracking_info(gedict_t *p); +// Wipeout spawn management functions +static wipeout_spawn_config* WO_FindSpawnConfig(vec3_t origin); +float WO_GetSpawnRadius(vec3_t origin); +void WO_InitializeSpawns(void); + gedict_t* ca_find_player(gedict_t *p, gedict_t *observer) { char *team = getteam(observer); @@ -101,6 +137,8 @@ int calc_respawn_time(gedict_t *p, int offset) teamsize++; } + p->is_solo = teamsize == 1 ? 1 : 0; + multiple = bound(3, teamsize+1, 6); // first respawn won't take more than 6 seconds regardless of team size if (isWipeout && (p->round_deaths+offset <= max_deaths)) @@ -111,6 +149,11 @@ int calc_respawn_time(gedict_t *p, int offset) time = p->round_deaths+offset == 1 ? multiple : (p->round_deaths-1+offset) * (multiple*2); } + // If you're the only player on your team, you get one free instant respawn on first death + if (isWipeout && p->is_solo && p->round_deaths+offset == 1) { + time = 0; + } + return time; } @@ -219,6 +262,11 @@ void SM_PrepareCA(void) return; } + if (cvar("k_clan_arena") == 2) + { + WO_InitializeSpawns(); // init wipeout spawns + } + team1_score = team2_score = 0; round_num = 1; @@ -247,6 +295,7 @@ qbool isCA(void) return (isTeam() && cvar("k_clan_arena")); } +// Used to determine value of ca_alive when PutClientInServer() is called qbool CA_CheckAlive(gedict_t *p) { if (p) @@ -273,6 +322,22 @@ qbool CA_CheckAlive(gedict_t *p) } } +void CA_AddLatePlayer(gedict_t *p, char *team) +{ + p->ready = 1; + p->ca_ready = 1; + p->lj_accepted = 1; // Set flag to allow team change in FixPlayerTeam + + SetUserInfo(p, "team", team, 0); + stuffcmd_flags(p, STUFFCMD_IGNOREINDEMO, "team \"%s\"\n", team); + G_bprint(2, "%s late-joined team \x90%s\x91\n", p->netname, team); + + p->lj_accepted = 0; // clear the flag immediately + p->ljteam[0] = '\0'; // clear the requested team name + p->can_respawn = false; // can't join mid-round + p->seconds_to_respawn = 999; // don't show countdown +} + void CA_MatchBreak(void) { gedict_t *p; @@ -301,10 +366,6 @@ void CA_MatchBreak(void) void track_player(gedict_t *observer) { gedict_t *player = ca_get_player(observer); - vec3_t delta; - float vlen; - int follow_distance; - int upward_distance; if (player && !observer->in_play && observer->tracking_enabled) { @@ -331,37 +392,12 @@ void track_player(gedict_t *observer) observer->track_target = player; } - // { spectate in 1st person - follow_distance = -10; - upward_distance = 0; - observer->hideentity = EDICT_TO_PROG(player); // in this mode we want to hide player model for watcher's view - VectorCopy(player->s.v.v_angle, observer->s.v.angles); - // } - - observer->s.v.fixangle = true; // force client v_angle (disable in 3rd person view) + // Use trackent for smooth tracking + observer->trackent = NUM_FOR_EDICT(player); + observer->hideentity = EDICT_TO_PROG(player); // Hide tracked player model - trap_makevectors(player->s.v.angles); - VectorMA(player->s.v.origin, follow_distance, g_globalvars.v_forward, observer->s.v.origin); - VectorMA(observer->s.v.origin, upward_distance, g_globalvars.v_up, observer->s.v.origin); - - // avoid positionning in walls - traceline(PASSVEC3(player->s.v.origin), PASSVEC3(observer->s.v.origin), false, player); - VectorCopy(g_globalvars.trace_endpos, observer->s.v.origin); - - if (g_globalvars.trace_fraction == 1) - { - VectorCopy(g_globalvars.trace_endpos, observer->s.v.origin); - VectorMA(observer->s.v.origin, 10, g_globalvars.v_forward, observer->s.v.origin); - } - else - { - VectorSubtract(g_globalvars.trace_endpos, player->s.v.origin, delta); - vlen = VectorLength(delta); - vlen = vlen - 40; - VectorNormalize(delta); - VectorScale(delta, vlen, delta); - VectorAdd(player->s.v.origin, delta, observer->s.v.origin); - } + // Lock observer's orientation to player POV + observer->s.v.movetype = MOVETYPE_LOCK; // set observer's health/armor/ammo/weapon to match the player's observer->s.v.ammo_nails = player->s.v.ammo_nails; @@ -377,17 +413,14 @@ void track_player(gedict_t *observer) observer->weaponmodel = player->weaponmodel; observer->s.v.weaponframe = player->s.v.weaponframe; - // smooth playing for ezq / zq - observer->s.v.movetype = MOVETYPE_LOCK; - show_tracking_info(observer); } - - if (!player || !observer->tracking_enabled) + else { - // restore movement and show racer entity - observer->s.v.movetype = MOVETYPE_NOCLIP; + // Clear tracking + observer->trackent = 0; observer->hideentity = 0; + observer->s.v.movetype = MOVETYPE_NOCLIP; // set health/item values back to nothing observer->s.v.ammo_nails = 0; @@ -522,9 +555,10 @@ void CA_PutClientInServer(void) // previous round will be invisible self->hideentity = 0; - // reset escape time and last_alive every spawn + // reset escape_time, last_alive, and regen_timer every spawn self->escape_time = 0; self->last_alive_active = false; + self->regen_timer = 0; // default to spawning with rl self->s.v.weapon = IT_ROCKET_LAUNCHER; @@ -575,6 +609,7 @@ void CA_PutClientInServer(void) // tracking enabled by default self->tracking_enabled = 1; + self->trackent = 0; // Initialize trackent for dead players self->in_play = false; self->round_deaths++; //increment death count for wipeout @@ -700,9 +735,9 @@ void CA_SendTeamInfo(gedict_t *t) break; } - if (t->trackent && (t->trackent == NUM_FOR_EDICT(p))) + if (t->ct == ctSpec && t->trackent && (t->trackent == NUM_FOR_EDICT(p))) { - continue; // we pseudo speccing such player, no point to send info about him + continue; // if we're spectating the player, don't send info about him } if (p->ca_ready || match_in_progress != 2) // be sure to send info if in prewar @@ -797,7 +832,43 @@ void CA_check_escape(gedict_t *targ, gedict_t *attacker) // That's cool, but could be written cleaner in calc_respawn_time(). targ->round_deaths--; - G_bprint(2, "%s survives by &cff0%.3f&r seconds!\n", targ->netname, escape_time); + G_bprint(2, "%s survives by &cff0%.0f&r milliseconds!\n", targ->netname, escape_time*1000); + } +} + +// wipeout: solo players (one-man teams) who don't take damage for 5 seconds +// after earning a frag get their health/armor/ammo regenerated. +void check_solo_regen(gedict_t *p) +{ + int required_time = p->round_kills * 5; + int time_since_kill = p->regen_timer ? g_globalvars.time - p->regen_timer : 0; + + if (!p->is_solo) + { + return; + } + + if (p->regen_timer && time_since_kill > required_time) + { + // regenerate health/armore/ammo. play megahealth sound or secret sound + stuffcmd(p, "play misc/secret.wav\n"); + + if (!((int)self->s.v.items & IT_ARMOR3)) + { + p->s.v.items += IT_ARMOR3; + } + + p->s.v.armorvalue = 200; + p->s.v.armortype = 0.8; + p->s.v.health = 100; + p->s.v.ammo_nails = 200; + p->s.v.ammo_shells = 100; + p->s.v.ammo_rockets = 50; + p->s.v.ammo_cells = 150; + p->ca_ammo_grenades = 6; + + // reset the timer + p->regen_timer = 0; } } @@ -807,6 +878,12 @@ void CA_ClientObituary(gedict_t *targ, gedict_t *attacker) if (cvar("k_clan_arena") == 2) // Wipeout only { + // check if attacker is a solo player and start regen timer + if (attacker->is_solo && !attacker->regen_timer) + { + attacker->regen_timer = g_globalvars.time; + } + // check if targ was a lone survivor waiting for teammate to spawn CA_check_escape(targ, attacker); } @@ -846,6 +923,39 @@ void CA_ClientObituary(gedict_t *targ, gedict_t *attacker) // } } +// check if a team is a solo player on first death +static qbool is_solo_team_first_death(char *team) +{ + gedict_t *p; + int team_size = 0; + gedict_t *team_player = NULL; + + if (!team || cvar("k_clan_arena") != 2) // Only applies to wipeout + { + return false; + } + + for (p = world; (p = find_plr_same_team(p, team));) + { + if (p->ca_ready) + { + team_player = p; + team_size++; + + if (team_size > 1) + { + return false; + } + } + } + + return (team_player + && team_player->is_solo // redundant but free, so why not + && team_player->round_deaths <= 1 // "<= 1" avoids a race condition + && team_player->can_respawn // makes sures player didn't /kill + ); +} + // return 0 if there no alive teams // return 1 if there one alive team and alive_team point to 1 or 2 wich refering to _k_team1 or _k_team2 cvars // return 2 if there at least two alive teams @@ -854,6 +964,7 @@ static int CA_check_alive_teams(int *alive_team) gedict_t *p; qbool few_alive_teams = false; char *first_team = NULL; + char *dead_team = NULL; if (alive_team) { @@ -894,8 +1005,26 @@ static int CA_check_alive_teams(int *alive_team) *alive_team = streq(first_team, cvar_string("_k_team1")) ? 1 : 2; } + // Wipeout only: + // Check if the "dead" team is actually a solo player on first death + dead_team = streq(first_team, cvar_string("_k_team1")) ? cvar_string("_k_team2") : cvar_string("_k_team1"); + if (is_solo_team_first_death(dead_team)) + { + return 2; // Both teams still in play - solo player gets instant respawn + } + return 1; } + else + { + // Wipeout only: + // Both teams are "dead" but one or both teams may be a solo player on his first death + if (is_solo_team_first_death(cvar_string("_k_team1")) || + is_solo_team_first_death(cvar_string("_k_team2"))) + { + return 2; // At least one team consists of a solo player on first death + } + } return 0; } @@ -1103,10 +1232,10 @@ void EndRound(int alive_team) "%s", ((alive_team == 1 && team1_score == (CA_wins_required()-1)) || (alive_team == 2 && team2_score == (CA_wins_required()-1))) ? "series" : "round"); - if ((loser_respawn_time < 2) && (loser_respawn_time > 0)) + if ((loser_respawn_time < 1) && (loser_respawn_time > 0)) { - G_cp2all("Team \x90%s\x91 wins the %s!\n\n\nTeam %s needed %.3f more seconds", - cvar_string(va("_k_team%d", alive_team)), round_or_series, cvar_string(va("_k_team%d", loser_team)), loser_respawn_time); + G_cp2all("Team \x90%s\x91 wins the %s!\n\n\nTeam %s needed %.0f ms to respawn", + cvar_string(va("_k_team%d", alive_team)), round_or_series, cvar_string(va("_k_team%d", loser_team)), loser_respawn_time*1000); } else { G_cp2all("Team \x90%s\x91 wins the %s!", @@ -1249,17 +1378,15 @@ void CA_player_pre_think(void) { if (isCA()) { + float alive_time = self->in_play ? g_globalvars.time - self->time_of_respawn : 0; + CA_show_greeting(self); - // Set this player to solid so we trigger checkpoints & teleports during move - self->s.v.solid = (ISDEAD(self) ? SOLID_NOT : SOLID_SLIDEBOX); - if ((self->s.v.mins[0] == 0) || (self->s.v.mins[1] == 0)) { // This can happen if the world 'squashes' a SOLID_NOT entity, mvdsv will turn into corpse setsize(self, PASSVEC3(VEC_HULL_MIN), PASSVEC3(VEC_HULL_MAX)); } - setorigin(self, PASSVEC3(self->s.v.origin)); if ((self->ct == ctPlayer) && (ISDEAD(self) || !self->in_play)) @@ -1287,13 +1414,14 @@ void CA_player_pre_think(void) track_player(self); // enable tracking by default while dead } - if (self->in_play) + // wipeout: if you're a solo and waiting for health regen + if (cvar("k_clan_arena") == 2 && self->is_solo && self->regen_timer) { - self->alive_time = g_globalvars.time - self->time_of_respawn; + check_solo_regen(self); } // take no damage to health/armor within 1 second of respawn or during endround - if (self->in_play && ((self->alive_time >= 1) || !self->round_deaths) && !ca_round_pause) + if (self->in_play && ((alive_time >= 1) || !self->round_deaths) && !ca_round_pause) { self->no_pain = false; } @@ -1329,26 +1457,34 @@ void CA_player_pre_think(void) void CA_spectator_think(void) { - gedict_t *p; + gedict_t *target, *teammate; + int id; - p = PROG_TO_EDICT(self->s.v.goalentity); // who we are spectating + target = PROG_TO_EDICT(self->s.v.goalentity); // who we are spectating - if (p->ct == ctPlayer && !p->in_play && p->tracking_enabled) + // If spectating a dead player, switch to an alive teammate + if (target && target->ct == ctPlayer && !target->in_play) { - // if the player you're observing is following someone else, hide the player model - self->hideentity = EDICT_TO_PROG(p->track_target); - } - else - { - self->hideentity = 0; + // Find any alive teammate + teammate = ca_find_player(world, target); + if (teammate && teammate->in_play && teammate != target) + { + // Use stuffcmd to switch the spectator to the alive teammate + if ((id = GetUserID(teammate)) > 0) + { + stuffcmd_flags(self, STUFFCMD_IGNOREINDEMO, "track %d\n", id); + } + } } - if (p->ct == ctPlayer) + // Get the current viewing target (may have changed due to stuffcmd) + target = PROG_TO_EDICT(self->s.v.goalentity); + if (target && target->ct == ctPlayer) { if (match_in_progress == 2 && ra_match_fight == 2 && round_time > 2 && !ca_round_pause) { // any centerprint the player sees is sent to the spec - G_centerprint(self, "%s\n", p->cptext); + G_centerprint(self, "%s\n", target->cptext); } } } @@ -1376,6 +1512,7 @@ void CA_Frame(void) // if k_clan_arena is 2, we're playing wipeout if (ra_match_fight == 2 && !ca_round_pause && cvar("k_clan_arena") == 2) { + float alive_time; int last_alive; int e_last_alive; char str_last_alive[25]; @@ -1383,6 +1520,7 @@ void CA_Frame(void) for (p = world; (p = find_plr(p));) { + alive_time = p->in_play ? g_globalvars.time - p->time_of_respawn : 0; last_alive = (int)ceil(last_alive_time(p)); e_last_alive = (int)ceil(enemy_last_alive_time(p)); @@ -1404,7 +1542,7 @@ void CA_Frame(void) k_respawn(p, true); p->seconds_to_respawn = calc_respawn_time(p, 1); - p->time_of_respawn = g_globalvars.time; // resets alive_time to 0 + p->time_of_respawn = g_globalvars.time; // resets alive_time calculations to 0 } else { @@ -1416,8 +1554,17 @@ void CA_Frame(void) } } } + // you're a solo player... prioritize regen info + else if (p->is_solo && p->regen_timer) + { + int countdown = max(1, (p->round_kills*5) - (int)(g_globalvars.time - p->regen_timer) + 1 ); + snprintf(p->cptext, sizeof(p->cptext), "\n\n\n\n\n\n%s: %d\n\n\n\n", + "regenerating health", countdown); + + G_centerprint(p, "%s", p->cptext); + } // both you and the enemy are the last alive on your team - else if (p->in_play && p->alive_time > 2 && last_alive && e_last_alive) + else if (p->in_play && alive_time > 2 && last_alive && e_last_alive) { snprintf(str_last_alive, sizeof(str_last_alive), "%d", last_alive); snprintf(p->cptext, sizeof(p->cptext), "\n\n\n\n\n\n%s\n\n\n%s\n\n\n\n", @@ -1426,7 +1573,7 @@ void CA_Frame(void) G_centerprint(p, "%s", p->cptext); } // you're the last alive on your team versus two or more enemies... hide! - else if (p->in_play && p->alive_time > 2 && last_alive) + else if (p->in_play && alive_time > 2 && last_alive) { snprintf(str_last_alive, sizeof(str_last_alive), "%d", last_alive); snprintf(p->cptext, sizeof(p->cptext), "\n\n\n\n\n\n%s\n\n\n%s\n\n\n\n", @@ -1435,7 +1582,7 @@ void CA_Frame(void) G_centerprint(p, "%s", p->cptext); } // only one enemy remains... find him! - else if (p->in_play && p->alive_time > 2 && e_last_alive) + else if (p->in_play && alive_time > 2 && e_last_alive) { snprintf(str_e_last_alive, sizeof(str_e_last_alive), "%d", e_last_alive); snprintf(p->cptext, sizeof(p->cptext), "\n\n\n\n\n\n%s\n\n\n%s\n\n\n\n", @@ -1443,7 +1590,7 @@ void CA_Frame(void) G_centerprint(p, "%s", p->cptext); } - else if (p->in_play && p->alive_time > 2) + else if (p->in_play && alive_time > 2) { snprintf(p->cptext, sizeof(p->cptext), " "); G_centerprint(p, "%s", p->cptext); @@ -1451,7 +1598,7 @@ void CA_Frame(void) } } - // check if there exist only one team with alive players and others are eluminated, if so then its time to start ca countdown + // check if there exist only one team with alive players and others are eliminated, if so then its time to start ca countdown if (ra_match_fight == 2 || (ra_match_fight == 1 && ca_round_pause == 1)) { int alive_team = 0; @@ -1530,7 +1677,7 @@ void CA_Frame(void) if (p->ca_ready) { - p->time_of_respawn = g_globalvars.time; // resets alive_time to 0 + p->time_of_respawn = g_globalvars.time; // resets alive_time calculations to 0 } } @@ -1576,3 +1723,68 @@ void CA_Frame(void) } } } + +// Find spawn configuration for a given origin +static wipeout_spawn_config* WO_FindSpawnConfig(vec3_t origin) +{ + int i, j; + + if (cvar("k_clan_arena") != 2) // Only for wipeout mode + { + return NULL; + } + + // Find current map configuration + for (i = 0; wipeout_spawn_configs[i].mapname; i++) + { + if (streq(mapname, wipeout_spawn_configs[i].mapname)) + { + // Search for matching spawn point + for (j = 0; j < wipeout_spawn_configs[i].spawn_count; j++) + { + if (VectorCompare(origin, wipeout_spawn_configs[i].spawns[j].origin)) + { + return &wipeout_spawn_configs[i].spawns[j]; + } + } + break; + } + } + + return NULL; +} + +// Get custom spawn radius for a spawn point +float WO_GetSpawnRadius(vec3_t origin) +{ + wipeout_spawn_config *config = WO_FindSpawnConfig(origin); + + if (config && config->custom_radius > 0) + { + return config->custom_radius; + } + + return 0; // Use default radius +} + +// Initialize wipeout spawns (can be called to reload configurations) +void WO_InitializeSpawns(void) +{ + if (cvar("k_clan_arena") == 2) + { + int i; + for (i = 0; wipeout_spawn_configs[i].mapname; i++) + { + if (streq(mapname, wipeout_spawn_configs[i].mapname)) + { + if (cvar("developer")) + { + G_bprint(2, "Wipeout: Using custom spawn configuration for %s (%d spawns)\n", + mapname, wipeout_spawn_configs[i].spawn_count); + } + + break; + } + } + } +} diff --git a/src/client.c b/src/client.c index a8b8abad..f39675ce 100644 --- a/src/client.c +++ b/src/client.c @@ -57,6 +57,8 @@ void SendSpecInfo(gedict_t *spec, gedict_t *target_client); void del_from_specs_favourites(gedict_t *rm); void item_megahealth_rot(void); +float WO_GetSpawnRadius(vec3_t origin); + extern int g_matchstarttime; void CheckAll(void) @@ -1012,6 +1014,26 @@ float CheckSpawnPoint(vec3_t v) return false; } +/* + ============ + GetEffectiveSpawnRadius + + Returns the effective spawn radius for a spawn point, considering wipeout custom radius + ============ + */ +static float GetEffectiveSpawnRadius(gedict_t *spot, float default_radius) +{ + if (cvar("k_clan_arena") == 2) + { + float custom_radius = WO_GetSpawnRadius(spot->s.v.origin); + if (custom_radius > 0) + { + return custom_radius; + } + } + return default_radius; +} + /* ============ SelectSpawnPoint @@ -1029,6 +1051,7 @@ gedict_t* Sub_SelectSpawnPoint(char *spawnname) int pcount; int k_spw = cvar("k_spw"); int weight_sum = 0; // used by "fair spawns" + float spawn_radius = 84; // default spawn check radius // testinfo_player_start is only found in regioned levels spot = find(world, FOFCLSN, "testplayerstart"); @@ -1057,17 +1080,32 @@ gedict_t* Sub_SelectSpawnPoint(char *spawnname) for (spot = world; (spot = find(spot, FOFCLSN, spawnname));) { + float spot_radius; + totalspots++; pcount = 0; + // Get spawn-specific radius if defined + spot_radius = GetEffectiveSpawnRadius(spot, spawn_radius); + // find count of nearby players for 'spot' - for (thing = world; (thing = trap_findradius(thing, spot->s.v.origin, 84));) + for (thing = world; (thing = trap_findradius(thing, spot->s.v.origin, spot_radius));) { if ((thing->ct != ctPlayer) || ISDEAD(thing) || (thing == self)) { continue; // ignore non player, or dead played, or self } + // For wipeout mode, check line of sight before blocking spawn + if (cvar("k_clan_arena") == 2) + { + traceline(PASSVEC3(spot->s.v.origin), PASSVEC3(thing->s.v.origin), true, self); + if (g_globalvars.trace_fraction < 1) + { + continue; // Wall/obstruction between spawn and player, don't discard spawn + } + } + // k_spw 2 and 3 and 4 feature, if player is spawned not far away and run // around spot - treat this spot as not valid. // k_1spawn store this "not far away" time. @@ -1129,16 +1167,30 @@ gedict_t* Sub_SelectSpawnPoint(char *spawnname) if (!match_in_progress || k_spw == 1 || (k_spw == 2 && !k_checkx)) { vec3_t v1, v2; + float fallback_radius; trap_makevectors(isRA() ? spot->mangle : spot->s.v.angles); // stupid ra uses mangles instead of angles - for (thing = world; (thing = trap_findradius(thing, spot->s.v.origin, 84));) + // Get spawn-specific radius for fallback spawn too + fallback_radius = GetEffectiveSpawnRadius(spot, spawn_radius); + + for (thing = world; (thing = trap_findradius(thing, spot->s.v.origin, fallback_radius));) { if ((thing->ct != ctPlayer) || ISDEAD(thing) || (thing == self)) { continue; // ignore non player, or dead played, or self } + // For wipeout mode, check line of sight before moving player + if (cvar("k_clan_arena") == 2) + { + traceline(PASSVEC3(spot->s.v.origin), PASSVEC3(thing->s.v.origin), true, self); + if (g_globalvars.trace_fraction < 1) + { + continue; // Wall/obstruction between spawn and player, don't move them + } + } + VectorMA(thing->s.v.origin, -15.0, g_globalvars.v_up, v1); VectorMA(v1, 160.0, g_globalvars.v_forward, v2); @@ -1740,7 +1792,7 @@ void PutClientInServer(void) self->classname = "player"; self->s.v.health = 100; self->s.v.takedamage = DAMAGE_AIM; - self->s.v.solid = self->leavemealone ? SOLID_TRIGGER : SOLID_SLIDEBOX; + self->s.v.solid = isCA() ? SOLID_NOT : self->leavemealone ? SOLID_TRIGGER : SOLID_SLIDEBOX; self->s.v.movetype = MOVETYPE_WALK; self->show_hostile = 0; self->s.v.max_health = 100; @@ -1939,6 +1991,37 @@ void PutClientInServer(void) } } + if (isCA()) + { + CA_PutClientInServer(); + W_SetCurrentAmmo(); // important shit, not only ammo + teleport_player(self, self->s.v.origin, self->s.v.angles, tele_flags); + + g_globalvars.msg_entity = EDICT_TO_PROG(self); + WriteByte(MSG_ONE, 38 /*svc_updatestatlong*/); + WriteByte(MSG_ONE, 18 /*STAT_MATCHSTARTTIME*/); + WriteLong(MSG_ONE, g_matchstarttime); + +#ifdef BOT_SUPPORT + BotClientEntersEvent(self, spot); +#endif + + // dusty: CA/wipeout must set solid state AFTER the spawn/teleport_player() + // otherwise player will become "solid" while tracking other players and + // get hit by projectiles. + if (match_in_progress) + { + self->s.v.solid = self->in_play ? SOLID_SLIDEBOX : SOLID_NOT; + } + else + { + self->s.v.solid = self->leavemealone ? SOLID_TRIGGER : SOLID_SLIDEBOX; + } + setorigin(self, PASSVEC3(self->s.v.origin)); + + return; + } + if (isRA()) { ra_PutClientInServer(); @@ -2259,11 +2342,6 @@ void PutClientInServer(void) } } - if (isCA()) - { - CA_PutClientInServer(); - } - // remove particular weapons in dmm4 if (deathmatch == 4 && match_in_progress == 2) { @@ -2817,6 +2895,7 @@ void set_important_fields(gedict_t *p) void ClientDisconnect(void) { extern void mv_stop_playback(void); + gedict_t *spec; k_nochange = 0; // force recalculate frags scores @@ -2831,6 +2910,25 @@ void ClientDisconnect(void) del_from_specs_favourites(self); + // Clean up spectators tracking this player + for (spec = world; (spec = find_client(spec));) + { + if (spec->ct == ctSpec) + { + // Check if spectator is tracking the disconnecting player + if (spec->trackent == NUM_FOR_EDICT(self)) + { + spec->trackent = 0; + } + + // Check if spectator's goalentity is the disconnecting player + if (PROG_TO_EDICT(spec->s.v.goalentity) == self) + { + spec->s.v.goalentity = EDICT_TO_PROG(world); + } + } + } + ra_ClientDisconnect(); if (match_in_progress == 2 && self->ct == ctPlayer) diff --git a/src/combat.c b/src/combat.c index cd4d4a97..b02178af 100644 --- a/src/combat.c +++ b/src/combat.c @@ -444,6 +444,7 @@ void T_Damage(gedict_t *targ, gedict_t *inflictor, gedict_t *attacker, float dam float native_damage = damage; // save damage before apply any modificator char *attackerteam, *targteam, *attackername, *victimname; qbool tp4teamdmg = false; + qbool isWipeout = cvar("k_clan_arena") == 2; //midair and instagib float playerheight = 0, midheight = 0; @@ -523,6 +524,19 @@ void T_Damage(gedict_t *targ, gedict_t *inflictor, gedict_t *attacker, float dam return; } } + + // Wipeout solo players only: + // damage was taken, so reset regen timer if one was started + if (isWipeout && targ->is_solo && targ->regen_timer && attacker != targ) + { + // allow a second of forgiveness, otherwise the same fight + // that started the timer might also stop it. + if (g_globalvars.time - targ->regen_timer > 1.0) + { + stuffcmd(targ, "play boss2/idle.wav\n"); + targ->regen_timer = 0; + } + } } if ((int)cvar("k_midair")) diff --git a/src/commands.c b/src/commands.c index eb226393..d63a93c1 100644 --- a/src/commands.c +++ b/src/commands.c @@ -210,6 +210,7 @@ void iplist(void); void dmgfrags(void); void no_lg(void); void no_gl(void); +void latejoin(void); void mv_cmd_playback(void); void mv_cmd_record(void); void mv_cmd_stop(void); @@ -457,6 +458,7 @@ const char CD_NODESC[] = "no desc"; #define CD_HDPTOGGLE "toggle allow handicap" #define CD_HANDICAP "toggle handicap level" #define CD_NOWEAPON "toggle allow any weapon" +#define CD_LATEJOIN "join a team after the game started" #define CD_CAM "camera help text" #define CD_TRACKLIST "trackers list" #define CD_FPSLIST "fps list" @@ -831,6 +833,7 @@ cmd_t cmds[] = { "hdptoggle", hdptoggle, 0, CF_BOTH_ADMIN, CD_HDPTOGGLE }, { "handicap", handicap, 0, CF_PLAYER | CF_PARAMS | CF_MATCHLESS, CD_HANDICAP }, { "noweapon", noweapon, 0, CF_PLAYER | CF_PARAMS | CF_SPC_ADMIN, CD_NOWEAPON }, + { "latejoin", latejoin, 0, CF_PLAYER | CF_PARAMS | CF_SPC_ADMIN, CD_LATEJOIN }, { "cam", ShowCamHelp, 0, CF_SPECTATOR | CF_MATCHLESS, CD_CAM }, @@ -2313,7 +2316,7 @@ void ModStatusVote(void) } } -if (!match_in_progress) + if (!match_in_progress) { if ((votes = get_votes(OV_HOOKCLASSIC))) { @@ -5282,6 +5285,97 @@ void no_gl(void) stuffcmd_flags(self, STUFFCMD_IGNOREINDEMO, "cmd noweapon gl\n"); } +void latejoin(void) +{ + int till; + char arg_2[1024]; + gedict_t *p, *electguard; + int team_players = 0, other_team_players = 0; + + if (!match_in_progress) { + return; + } + + if (!isCA()) { + G_sprint(self, 2, "Late-join requests are only allowed during CA or Wipeout.\n"); + return; + } + + if (self->ca_ready) { + G_sprint(self, 2, "You're already on a team.\n"); + return; + } + + // Check if player is already being elected + if (is_elected(self, etLateJoin)) { + G_bprint(2, "%s %s!\n", self->netname, redtext("aborts late join request")); + AbortElect(); + return; + } + + // Check if any election is in progress + if (get_votes(OV_ELECT)) { + G_sprint(self, 2, "An election is already in progress\n"); + return; + } + + // Check election timeout + if ((till = Q_rint(self->v.elect_block_till - g_globalvars.time)) > 0) { + G_sprint(self, 2, "Wait %d second%s!\n", till, count_s(till)); + return; + } + + // Get team argument + if (trap_CmdArgc() < 2) { + G_sprint(self, 2, "Usage: latejoin \n"); + G_sprint(self, 2, "Available teams: %s, %s \n", cvar_string("_k_team1"), cvar_string("_k_team2")); + return; + } + + trap_CmdArgv(1, arg_2, sizeof(arg_2)); + + // Validate team name + if (!streq(arg_2, cvar_string("_k_team1")) && !streq(arg_2, cvar_string("_k_team2"))) { + G_sprint(self, 2, "Invalid team. Must be %s or %s \n", cvar_string("_k_team1"), cvar_string("_k_team2")); + return; + } + + // Count players on each team + for (p = world; (p = find_plr(p));) { + if (p->ca_ready) { + if (streq(getteam(p), arg_2)) { + team_players++; + } else { + other_team_players++; + } + } + } + + if (team_players > other_team_players) { + G_sprint(self, 2, "Team %s already has more players\n", arg_2); + return; + } + + // Store the requested team for later use + snprintf(self->ljteam, sizeof(self->ljteam), arg_2); + //self->ljteam = arg_2; + + // Start the election + self->v.elect = 1; + self->v.elect_type = etLateJoin; + + G_bprint(2, "%s has requested to %s team \x90%s\x91\n", + self->netname, redtext("late-join"), arg_2); + G_bprint(2, "Team \x90%s\x91 members: type %s to approve\n", arg_2, redtext("yes")); + + // Spawn election timeout entity + electguard = spawn(); + electguard->s.v.owner = EDICT_TO_PROG(world); + electguard->classname = "electguard"; + electguard->think = (func_t) ElectThink; + electguard->s.v.nextthink = g_globalvars.time + 30; // 30 second timeout +} + void tracklist(void) { int i; diff --git a/src/g_userinfo.c b/src/g_userinfo.c index 087a8237..a2060094 100644 --- a/src/g_userinfo.c +++ b/src/g_userinfo.c @@ -388,7 +388,8 @@ qbool FixPlayerTeam(char *newteam) { // in CA/wipeout, non-participating players are forced to not have a team. // so we must allow team change if player isn't ca_ready and newteam is "" - if (self->ca_ready || strneq(newteam, "")) + // Exception: Allow team change for approved late join players (lj_accepted flag set by CA_AddLatePlayer) + if ((self->ca_ready || strneq(newteam, "")) && !(self->lj_accepted && streq(newteam, self->ljteam))) { G_sprint(self, 2, "You may %s change team during game\n", redtext("not")); stuffcmd_flags(self, STUFFCMD_IGNOREINDEMO, "team \"%s\"\n", getteam(self)); // sends this to client - so he get right team too diff --git a/src/vote.c b/src/vote.c index 01a85660..b1358e09 100644 --- a/src/vote.c +++ b/src/vote.c @@ -84,6 +84,7 @@ void ElectThink(void) void VoteYes(void) { int votes; + gedict_t *p; if (!get_votes(OV_ELECT)) { @@ -104,6 +105,27 @@ void VoteYes(void) return; } + // For late join elections, check if voter is on the requested team + if (get_elect_type() == etLateJoin) + { + // Find the player being elected + for (p = world; (p = find_client(p));) + { + if (is_elected(p, etLateJoin)) + { + char *requested_team = p->ljteam; + + // Only players on the requested team can vote + if (!self->ca_ready || !streq(getteam(self), requested_team)) + { + G_sprint(self, 2, "Only members of team %s can vote\n", requested_team); + return; + } + break; + } + } + } + // register the vote self->v.elect = 1; @@ -176,6 +198,40 @@ int get_votes_by_value(int fofs, int value) return votes; } +// Count votes from members of a specific team for late join +int get_latejoin_votes(char *team) +{ + int votes = 0; + gedict_t *p; + + for (p = world; (p = find_client(p));) + { + if (p->v.elect && p->ca_ready && streq(getteam(p), team)) + { + votes++; + } + } + + return votes; +} + +// Count eligible voters on a team for late join +int count_team_voters(char *team) +{ + int count = 0; + gedict_t *p; + + for (p = world; (p = find_plr(p));) + { + if (p->ca_ready && streq(getteam(p), team) && !p->s.v.owner) + { + count++; + } + } + + return count; +} + int get_votes_req(int fofs, qbool diff) { float percent = 51; @@ -232,11 +288,15 @@ int get_votes_req(int fofs, qbool diff) percent = cvar("k_vp_suggestcolor"); break; } + else if (el_type == etLateJoin) + { + percent = 51; // Default to 51% for late join + break; + } else { percent = 100; break; // unknown/none election - break; } break; @@ -283,7 +343,28 @@ int get_votes_req(int fofs, qbool diff) vt_req = ceil(percent * (CountPlayers() - CountBots())); } - if (fofs == OV_ELECT) + // Special handling for late join elections + if ((fofs == OV_ELECT) && (get_elect_type() == etLateJoin)) + { + gedict_t *p; + // Find the player being elected + for (p = world; (p = find_client(p));) + { + if (is_elected(p, etLateJoin)) + { + char *requested_team = p->ljteam; + if (requested_team[0]) + { + // Count only team votes and require majority from that team + votes = get_latejoin_votes(requested_team); + vt_req = ceil(percent * count_team_voters(requested_team)); + vt_req = max(1, vt_req); // at least 1 vote needed + } + break; + } + } + } + else if (fofs == OV_ELECT) { vt_req = max(2, vt_req); // if election, at least 2 votes needed } @@ -406,6 +487,11 @@ int get_elect_type(void) { return etSuggestColor; } + + if (is_elected(p, etLateJoin)) + { + return etLateJoin; + } } return etNone; @@ -426,6 +512,12 @@ char* get_elect_type_str(void) case etAdmin: return "Admin"; + + case etLateJoin: + return "Late Join"; + + case etSuggestColor: + return "Suggest Color"; } return "Unknown"; @@ -610,6 +702,19 @@ void vote_check_elect(void) } } + if (match_in_progress && isCA()) + { + if (is_elected(p, etLateJoin)) + { + // Get the requested team from userinfo + char *team = p->ljteam; + if (team[0]) + { + CA_AddLatePlayer(p, team); + } + } + } + AbortElect(); } }