1170 lines
31 KiB
Plaintext
1170 lines
31 KiB
Plaintext
/*
|
|
* Copyright (c) 2016-2020 Marco Cawthorne <marco@icculus.org>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
|
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
|
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::TSMultiplayerRules(void)
|
|
{
|
|
// Whoops! CS stuff
|
|
// Anything similar-looking for TS team-based gamemodes could go here then.
|
|
/*
|
|
forceinfokey(world, "teams", "2");
|
|
forceinfokey(world, "team_1", "Specialists");
|
|
forceinfokey(world, "teamscore_1", "0");
|
|
forceinfokey(world, "team_2", "Mercenaries");
|
|
forceinfokey(world, "teamscore_2", "0");
|
|
*/
|
|
}
|
|
|
|
|
|
// respawn absolutely everything
|
|
// (check out init_respawn in case the var checked or any other part of the process changes)
|
|
void
|
|
TSMultiplayerRules::RespawnMain(void){
|
|
|
|
// TAGGG TODO - anyway to tell all clients to remove all tempents like debris effects from
|
|
// recently broken things? Not a big deal if this can't be done
|
|
|
|
/*
|
|
// WAIT! This has an issue, affects some things that shouldn't be, like CBaseTrigger's.
|
|
// the Respawn method resets the CBaseEntity's target to m_oldstrTarget, which is fine for map-loaded triggers
|
|
// but not temporary ones. They have blank oldstr's and so are disabled. Not quite sure why they're just
|
|
// not deleted on-the-spot so CBaseTrigger can regenerate them or something like that.
|
|
// Anyway, that is Nuclide (gs-entbase), but the point is to be careful about blindly respawning everything.
|
|
// For now, re-directing to the plain "RespawnRoutine" for respawning all breakables.
|
|
// If anything can potentially be out of alignment since round-start that should be returned that way,
|
|
// go ahead and call respawn for that classname too
|
|
for (entity a = world; (a = findfloat(a, ::identity, 1));) {
|
|
NSEntity ent = (NSEntity)a;
|
|
// Not a player, spectator, or viewmodel (if it's even possible to run into those this way)? Go ahead
|
|
if(
|
|
!(ent.classname == "player" || ent.classname == "spectator" || ent.classname == "vm")
|
|
|
|
){
|
|
//printfline("I REMOVED THAT %s", ent.classname);
|
|
ent.Respawn();
|
|
}
|
|
}
|
|
*/
|
|
|
|
// some entities used by ts_bikini at least,
|
|
|
|
/*
|
|
|| ent.classname == "cycler" || ent.classname == "trigger_multiple"
|
|
|| ent.classname == "multi_manager_sub"
|
|
|| ent.classname == "trigger_relay"
|
|
|| ent.classname == "func_illusionary"
|
|
|| ent.classname == "multi_manager"
|
|
|| ent.classname == "cycler_sprite"
|
|
|| ent.classname == "env_sprite"
|
|
|| ent.classname == "ambient_generic"
|
|
|| ent.classname == "func_wall"
|
|
|
|
|| ent.classname == "light"
|
|
|| ent.classname == "CBaseTrigger"
|
|
|
|
|| ent.classname == "info_notnull"
|
|
|| ent.classname == "func_conveyor"
|
|
|| ent.classname == "func_breakable"
|
|
|
|
//ent.classname == "CBaseTrigger"
|
|
*/
|
|
|
|
RespawnRoutine();
|
|
|
|
}//RespawnMain
|
|
|
|
// respawn only breakables every so often.
|
|
void
|
|
TSMultiplayerRules::RespawnRoutine(void){
|
|
// Thanks, you can stop now
|
|
//printfline("TSMultiplayerRules::TimerUpdate - I HAVE RESPAWNED THE BREAKABLES. time:%d : starttime:%d : nextrespawntime:%d", time, global_gameStartTime, global_nextBreakableRespawn);
|
|
|
|
//for (entity a = world; (a = findfloat(a, ::gflags, GF_CANRESPAWN));) {
|
|
for (entity a = world; (a = findfloat(a, ::identity, IDENTITY_CANRESPAWN));) {
|
|
NSEntity caw = (NSEntity)a;
|
|
if(caw.classname == "func_breakable"){
|
|
caw.Respawn();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::InitPostEnts(void)
|
|
{
|
|
/* let's check if we need to create buyzones */
|
|
// TAGGG - TODO. NOPE. We're TS, create whatever zones/triggers for
|
|
// working with team-game specific areas ('defuse the bomb', 'hack the computer', etc.)
|
|
|
|
// CreateCTBuyzones(), CreateTBuyzones(), etc. was here
|
|
}
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::FrameStart(void)
|
|
{
|
|
if (cvar("mp_timelimit"))
|
|
if (time >= (cvar("mp_timelimit") * 60)) {
|
|
IntermissionStart();
|
|
}
|
|
|
|
IntermissionCycle();
|
|
|
|
|
|
//TAGGG - NEW.
|
|
// from GameInput, just check here too
|
|
if (m_iIntermission) {
|
|
|
|
IntermissionEnd();
|
|
|
|
// for now, some mock-Nuclide-script. Don't depend on inputs not being read because the only player is in spectator
|
|
if (time >= m_flIntermissionTime + 5){
|
|
localcmd("restart\n");
|
|
}
|
|
|
|
//return;
|
|
}
|
|
//////////////////////////////////////////////////
|
|
|
|
|
|
//TAGGG - hook into that!
|
|
TimerUpdate();
|
|
}
|
|
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::PlayerPreFrame(NSClientPlayer pp)
|
|
{
|
|
TSGameRules::PlayerPreFrame(pp);
|
|
|
|
player pl = (player) pp;
|
|
|
|
//TAGGG - TODO. uh-oh. was that important..? find equivalent behavior if needed?
|
|
// the modern CS lacks it here too at least, that's a good sign we don't need to worry.
|
|
//BaseGun_ShotMultiplierUpdate();
|
|
|
|
if (pl.health <= 0)
|
|
return;
|
|
|
|
if (g_ts_gamestate == GAME_FREEZE) {
|
|
pl.flags |= FL_FROZEN;
|
|
} else {
|
|
pl.flags &= ~FL_FROZEN;
|
|
}
|
|
|
|
}
|
|
|
|
void
|
|
TSMultiplayerRules::PlayerPostFrame(NSClientPlayer pp)
|
|
{
|
|
// Anything else?
|
|
TSGameRules::PlayerPostFrame(pp);
|
|
}
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::PlayerDisconnect(NSClientPlayer pp)
|
|
{
|
|
//TAGGG - no parent method call? Really?
|
|
TSGameRules::PlayerDisconnect(pp);
|
|
|
|
if (health > 0){
|
|
PlayerDeath(pp);
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::PlayerDeath(NSClientPlayer pp)
|
|
{
|
|
TSGameRules::PlayerDeath(pp);
|
|
|
|
|
|
player pl = (player)pp;
|
|
|
|
if (g_dmg_iHitBody == BODY_HEAD) {
|
|
|
|
//TAGGG - TODO. It sounds like "headshot1.wav" would be more fitting for taking the finishing
|
|
// shot in the head, and headshot2.wav at varying pitches for any otherwise.
|
|
// Just an idea, unsure how the original game handled this though.
|
|
//sound(pl, CHAN_VOICE, sprintf("player/headshot%d.wav", floor((random() * 3) + 1)), 1, ATTN_NORM);
|
|
|
|
/*
|
|
//...of course the first one has no number too. yaaaaaaay.
|
|
int randomChoice = randomInRange_i(1, 2);
|
|
if(randomChoice == 1i){
|
|
sound(pl, CHAN_VOICE, "player/headshot.wav", 1, ATTN_NORM);
|
|
}else{
|
|
sound(pl, CHAN_VOICE, sprintf("player/headshot%i.wav", randomChoice), 1, ATTN_NORM);
|
|
}
|
|
*/
|
|
Sound_Play(pl, CHAN_VOICE, "player.die_headshot");
|
|
|
|
} else {
|
|
///sound(pl, CHAN_VOICE, sprintf("player/die%i.wav", randomInRange_i(1, 3)), 1, ATTN_NORM);
|
|
Sound_Play(pl, CHAN_VOICE, "player.die");
|
|
}
|
|
|
|
|
|
|
|
/* obituary networking */
|
|
WriteByte(MSG_MULTICAST, SVC_CGAMEPACKET);
|
|
WriteByte(MSG_MULTICAST, EV_OBITUARY);
|
|
if (g_dmg_eAttacker.netname)
|
|
WriteString(MSG_MULTICAST, g_dmg_eAttacker.netname);
|
|
else
|
|
WriteString(MSG_MULTICAST, g_dmg_eAttacker.classname);
|
|
WriteString(MSG_MULTICAST, pl.netname);
|
|
WriteByte(MSG_MULTICAST, g_dmg_iWeapon);
|
|
WriteByte(MSG_MULTICAST, 0);
|
|
msg_entity = world;
|
|
multicast([0,0,0], MULTICAST_ALL);
|
|
|
|
Plugin_PlayerObituary(g_dmg_eAttacker, g_dmg_eTarget, g_dmg_iWeapon, g_dmg_iHitBody, g_dmg_iDamage);
|
|
|
|
/* death-counter */
|
|
pl.deaths++;
|
|
forceinfokey(pl, "*deaths", ftos(pl.deaths));
|
|
|
|
/* update score-counter */
|
|
if (pl.flags & FL_CLIENT || pl.flags & FL_MONSTER)
|
|
if (g_dmg_eAttacker.flags & FL_CLIENT) {
|
|
if (pl == g_dmg_eAttacker)
|
|
g_dmg_eAttacker.frags--;
|
|
else
|
|
g_dmg_eAttacker.frags++;
|
|
}
|
|
|
|
/* in DM we only care about the frags */
|
|
if (cvar("mp_fraglimit"))
|
|
if (g_dmg_eAttacker.frags >= cvar("mp_fraglimit")) {
|
|
IntermissionStart();
|
|
}
|
|
|
|
|
|
/* either gib, or make a corpse */
|
|
if (pl.health < -50) {
|
|
FX_GibHuman(pl.origin, vectoangles(pl.origin - g_dmg_eAttacker.origin), g_dmg_iDamage * 2.0f);
|
|
} else {
|
|
/* Let's handle corpses on the clientside */
|
|
entity corpse = spawn();
|
|
setorigin(corpse, pl.origin + [0,0,32]);
|
|
setmodel(corpse, pl.model);
|
|
setsize(corpse, VEC_HULL_MIN, VEC_HULL_MAX);
|
|
corpse.movetype = MOVETYPE_TOSS;
|
|
corpse.solid = SOLID_TRIGGER;
|
|
corpse.modelindex = pl.modelindex;
|
|
|
|
|
|
//TAGGG - Check. Is ANIM_DIESIMPLE supported in TS playermodels?
|
|
corpse.frame = ANIM_DIESIMPLE;
|
|
//TAGGG - TODO. Pick from below like so instead?
|
|
// Some method in animation.qc to handle this sounds better.
|
|
/*
|
|
if (pl.flags & FL_CROUCHING) {
|
|
corpse.frame = ANIM_CROUCH_DIE;
|
|
} else {
|
|
switch (g_dmg_iHitBody) {
|
|
case BODY_HEAD:
|
|
corpse.frame = ANIM_DIE_HEAD;
|
|
break;
|
|
case BODY_STOMACH:
|
|
corpse.frame = ANIM_DIE_GUT;
|
|
break;
|
|
case BODY_LEGLEFT:
|
|
case BODY_ARMLEFT:
|
|
corpse.frame = ANIM_DIE_LEFT;
|
|
break;
|
|
case BODY_LEGRIGHT:
|
|
case BODY_ARMRIGHT:
|
|
corpse.frame = ANIM_DIE_RIGHT;
|
|
break;
|
|
default:
|
|
corpse.frame = ANIM_DEATH1 + floor(random() * 3);
|
|
break;
|
|
}
|
|
}
|
|
*/
|
|
|
|
corpse.angles = pl.angles;
|
|
corpse.velocity = pl.velocity;
|
|
corpse.colormap = pl.colormap;
|
|
|
|
//TAGGG - Stops the issue of re-doing animations after going out of view / back again.
|
|
corpse.pvsflags = PVSF_NOREMOVE;
|
|
//corpse.pvsflags = PVSF_IGNOREPVS; //we want to remove ourselves. guaranteed.
|
|
|
|
//TAGGG also fade out like in TS.
|
|
corpse.think = entity_beginCorpseFadeOut;
|
|
corpse.nextthink = time + 6;
|
|
|
|
}// excess negative health gib check
|
|
|
|
|
|
//printfline("setInventoryEquippedIndex Flag Z");
|
|
TS_resetViewModel(pl);
|
|
pl.setInventoryEquippedIndex(-1);
|
|
|
|
pl.resetZoom();
|
|
|
|
|
|
for(int i = pl.ary_myWeapons_softMax-1; i >= 0; i--){
|
|
pl.dropWeapon(i, TRUE);
|
|
}
|
|
|
|
//and drop our ammo pools... if we have any. (should)
|
|
if(pl.anyAmmoPoolNonEmpty()){
|
|
pl.dropAmmo();
|
|
}
|
|
|
|
|
|
forceinfokey(pl, "*dead", "1");
|
|
forceinfokey(pl, "*team", ftos(pl.team));
|
|
CountPlayers();
|
|
|
|
|
|
//TAGGG - yes, restart the money at death so it shows up in the buy menu.
|
|
// Bonuses for performance in some team-based game not considered yet, always the flat start
|
|
// amount given.
|
|
setPlayerMoneyDefault(pl);
|
|
|
|
|
|
// Let the player know they died (uhh what).
|
|
// This lets client game logic know so that spectator can be told to wait
|
|
// for a fresh key press instead of letting holding down (like dying while
|
|
// firing auto) issue a respawn order, the player might not want that.
|
|
// Also, is MULTICAST_ONE or _R wanted here?
|
|
WriteByte( MSG_MULTICAST, SVC_CGAMEPACKET );
|
|
WriteByte( MSG_MULTICAST, EVENT_TS::PLAYER_DEATH );
|
|
msg_entity = pl;
|
|
multicast( [0,0,0], MULTICAST_ONE_R );
|
|
|
|
|
|
// Player model disappears since the corpse entity took its place.
|
|
// Redundant for gibbing but doesn't hurt
|
|
MakePlayerInvisible(pl);
|
|
|
|
// Now doing this instead
|
|
//PlayerMakeSpectatorDelayed(pl);
|
|
|
|
pl.iState = PLAYER_STATE::DEAD_RECENT;
|
|
pl.deathCameraChangeTime = 2.5;
|
|
pl.minimumRespawnTime = autocvar_respawntime;
|
|
|
|
// Not done just for this player, still a check on all alive players
|
|
// that happens to involve this deceased player. Strange.
|
|
DeathCheck(pl);
|
|
|
|
}
|
|
|
|
|
|
|
|
float
|
|
TSMultiplayerRules::ConsoleCommand(NSClientPlayer pp, string cmd)
|
|
{
|
|
tokenize(cmd);
|
|
|
|
switch (argv(0)) {
|
|
case "bot_add":
|
|
Bot_AddQuick();
|
|
break;
|
|
default:
|
|
return (0);
|
|
}
|
|
|
|
return (1);
|
|
}
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::TimerBegin(float tleft, int mode)
|
|
{
|
|
if (mode == GAME_FREEZE) {
|
|
g_ts_gamestate = GAME_FREEZE;
|
|
} else if (mode == GAME_ACTIVE) {
|
|
g_ts_gamestate = GAME_ACTIVE;
|
|
} else if (mode == GAME_END) {
|
|
g_ts_gamestate = GAME_END;
|
|
} else if (mode == GAME_COMMENCING) {
|
|
g_ts_gamestate = GAME_COMMENCING;
|
|
} else if (mode == GAME_OVER) {
|
|
g_ts_gamestate = GAME_OVER;
|
|
}
|
|
|
|
g_ts_gametime = tleft;
|
|
}
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::TimerUpdate(void)
|
|
{
|
|
for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) {
|
|
//printfline("TimerUpdate: found player");
|
|
player thisRef = (player)eFind;
|
|
thisRef.frameThink_fromServer();
|
|
}
|
|
|
|
/*
|
|
// Wait. shouldn't that mean gamerules_singleplayer.qc is being used instead?
|
|
if (cvar("sv_playerslots") == 1) {
|
|
g_ts_gametime = -1;
|
|
return;
|
|
}
|
|
*/
|
|
|
|
if (g_ts_gamestate != GAME_INACTIVE) {
|
|
if(time >= global_nextBreakableRespawn){
|
|
// Every minute, respawn all breakables.
|
|
// TAGGG - TODO CRITICAL. Perhaps each breakable can respawn in 60 seconds since it's been broken
|
|
// instead, and still along round-restarts?
|
|
// No idea why original TS chose to respawn everything in minutely-increments regardless
|
|
// of when it was broken. As in if it's been 55 seconds into a game, break something, it comes
|
|
// back in 5 seconds. huh.
|
|
RespawnRoutine();
|
|
|
|
/*
|
|
//...and set the next time.
|
|
float relativeTime = global_nextBreakableRespawn - global_gameStartTime;
|
|
int solidFits = ((int)relativeTime) / ((int)BREAKABLE_RESPAWN_INTERVAL);
|
|
global_nextBreakableRespawn = global_gameStartTime + (solidFits * BREAKABLE_RESPAWN_INTERVAL) + BREAKABLE_RESPAWN_INTERVAL; //the next 60 minutes...
|
|
//printfline("TSMultiplayerRules::TimerUpdate - %i %.2f %.2f", solidFits, time, global_nextBreakableRespawn);
|
|
*/
|
|
global_nextBreakableRespawn = global_nextBreakableRespawn + BREAKABLE_RESPAWN_INTERVAL;
|
|
|
|
}
|
|
}
|
|
|
|
//printfline("TSMultiplayerRules::TimerUpdate - gamemode:%d time:%i", currentGameMode, (int)g_ts_gametime);
|
|
|
|
if(currentGameMode == TS_GameMode::DEATHMATCH){
|
|
if (g_ts_gamestate == GAME_INACTIVE) {
|
|
|
|
if(g_ts_player_all > 0){
|
|
//good, we can change
|
|
printfline("***TimerUpdate: SERVER GAME_INACTIVE. THE GAME IS NOW ACTIVE.");
|
|
|
|
//TODO - any special start-game criteria here like the other place that calls GAME_FREEZE?
|
|
|
|
RestartRound(TRUE); //don't reset any active players (like the one that let
|
|
// the game start like this).
|
|
//TimerBegin(autocvar_mp_roundtime * 60, GAME_ACTIVE);
|
|
//TimerBegin(autocvar_mp_freezetime, GAME_FREEZE);
|
|
TimerBegin(0, GAME_FREEZE);
|
|
|
|
}
|
|
}else if(g_ts_gamestate == GAME_ACTIVE){
|
|
|
|
if(g_ts_player_all <= 0){
|
|
//last player left? uh-oh.
|
|
printfline("***TimerUpdate: SERVER GAME_ACTIVE. THE GAME IS NOW INACTIVE.");
|
|
g_ts_gamestate = GAME_INACTIVE;
|
|
}else{
|
|
|
|
if (g_ts_gametime <= 0) {
|
|
TimeOut();
|
|
|
|
centerprintToAll("End of round, prepare for a new one...\n");
|
|
|
|
TimerBegin(5, GAME_END); // Round is over, 5 seconds til a new round starts
|
|
}
|
|
}//END OF any player(s) here check
|
|
|
|
}else if(g_ts_gamestate == GAME_END){
|
|
if(g_ts_player_all <= 0){
|
|
//last player left? uh-oh.
|
|
printfline("***TimerUpdate: SERVER GAME_END. THE GAME IS NOW INACTIVE.");
|
|
g_ts_gamestate = GAME_INACTIVE;
|
|
}else{
|
|
if (g_ts_gametime <= 0){
|
|
printfline("***TimerUpdate: SERVER GAME_END. Delay over, respawning...");
|
|
RestartRound(FALSE); //this may do it
|
|
|
|
//TimerBegin(autocvar_mp_freezetime, GAME_FREEZE);
|
|
TimerBegin(0, GAME_FREEZE);
|
|
}
|
|
}
|
|
}else if(g_ts_gamestate == GAME_FREEZE){
|
|
|
|
if(g_ts_player_all <= 0){
|
|
//last player left? uh-oh.
|
|
printfline("***TimerUpdate: SERVER GAME_FREEZE. THE GAME IS NOW INACTIVE.");
|
|
g_ts_gamestate = GAME_INACTIVE;
|
|
}else{
|
|
if (g_ts_gametime <= 0) {
|
|
TimerBegin(autocvar_mp_roundtime * 60, GAME_ACTIVE); // Unfreeze
|
|
// Anything else to do here at round start?
|
|
// Best leave that up to gamemode choice later.
|
|
}
|
|
}
|
|
}
|
|
}// gamemode check
|
|
|
|
|
|
if (g_ts_gamestate != GAME_OVER) {
|
|
if (cvar("mp_timelimit") > 0) {
|
|
|
|
if (time >= (cvar("mp_timelimit") * 60)) {
|
|
centerprintToAll("mp_timelimit expired, changing map...\n");
|
|
|
|
TimerBegin(5, GAME_OVER);
|
|
printfline("Did the gamemode become GAME_OVER? %d", (float)(g_ts_gamestate==GAME_OVER));
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
if(g_ts_gamestate == GAME_OVER){
|
|
printfline("GAME_OVER: what is the timer? %.2f", g_ts_gametime);
|
|
}
|
|
*/
|
|
|
|
// whoops! Leaving intermissions to nuclide now.
|
|
|
|
if (g_ts_gamestate == GAME_INACTIVE) {
|
|
return;
|
|
}
|
|
|
|
if (g_ts_gametime > 0) {
|
|
g_ts_gametime -= frametime;
|
|
if(g_ts_gametime < 0){
|
|
g_ts_gametime = 0;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
=================
|
|
RoundOver
|
|
|
|
This happens whenever an objective is complete or time is up
|
|
=================
|
|
*/
|
|
void
|
|
TSMultiplayerRules::RoundOver(int iTeamWon, int iMoneyReward, int fSilent)
|
|
{
|
|
|
|
// nothing yet
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
=================
|
|
RestartRound
|
|
|
|
Loop through all ents and handle them
|
|
=================
|
|
*/
|
|
void
|
|
TSMultiplayerRules::RestartRound(int iWipe)
|
|
{
|
|
|
|
//TAGGG - is that right?
|
|
BOOL startingGame = (iWipe == TRUE);
|
|
|
|
if(!startingGame){
|
|
// Spawn/Respawn everyone at their team position and give them $$$
|
|
|
|
// TAGGG - CRITICAL.
|
|
// If gamemodes where the starting cash isn't reset to a static amount at every spawn
|
|
// are ever intended, this has to be adjusted.
|
|
for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) {
|
|
//self = eFind;
|
|
player pl = (player)eFind;
|
|
|
|
if(pl.iState == PLAYER_STATE::SPAWNED){
|
|
PlayerMakePlayableWithDefaultMoney(pl);
|
|
}
|
|
}
|
|
|
|
// anything to do with spectators?
|
|
// Remember, any classname="spectator" is an authentic spectator, not players that can open the
|
|
// buymenu and click to spawn after.
|
|
|
|
}//END OF !startingGame check
|
|
|
|
|
|
// Clear the corpses/items
|
|
for (entity eFind = world; (eFind = find(eFind, ::classname, "remove_me"));) {
|
|
remove(eFind);
|
|
}
|
|
|
|
/*
|
|
//unwise...
|
|
for (entity eFind = world; (eFind = find(eFind, classname, "tempdecal"));) {
|
|
remove(eFind);
|
|
}
|
|
*/
|
|
//try this
|
|
// !!!
|
|
|
|
// TAGGG - update2020. not yet! See if we even need to, probably but just be safe.
|
|
// that is re-implement the old decal.c, info_decal edits.
|
|
//Decals_Reset();
|
|
|
|
for (entity eFind = world; (eFind = find(eFind, ::classname, "decal"));) {
|
|
remove(eFind);
|
|
}
|
|
for (entity eFind = world; (eFind = find(eFind, ::classname, "infodecal"));) {
|
|
remove(eFind);
|
|
}
|
|
|
|
/*
|
|
WriteByte( MSG_MULTICAST, SVC_CGAMEPACKET );
|
|
WriteByte( MSG_MULTICAST, EVENT_TS::TEST );
|
|
|
|
//msg_entity = world;
|
|
//multicast( [0,0,0], MULTICAST_ONE );
|
|
|
|
multicast([0,0,0], MULTICAST_ALL);
|
|
*/
|
|
|
|
|
|
/*
|
|
// actually not necessary, all map powerups get the same GF_CANRESPAWN flag anyway.
|
|
for (entity eFind = world; (eFind = find(eFind, classname, "ts_powerup"));) {
|
|
NSEntity caw = (NSEntity)eFind;
|
|
caw.Respawn();
|
|
}
|
|
*/
|
|
|
|
|
|
// Respawn all the entities
|
|
RespawnMain();
|
|
|
|
// We'll respawn all breakables a minute from now, and so on
|
|
global_gameStartTime = time;
|
|
global_nextBreakableRespawn = time + BREAKABLE_RESPAWN_INTERVAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// From FreeCS, to check if all players on a team are dead for doing something
|
|
// gamerules-wise like ending in the other side's favor.
|
|
void
|
|
TSMultiplayerRules::DeathCheck(NSClientPlayer pl)
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
// makes more sense for team-based gamemodes, not touched yet.
|
|
void
|
|
TSMultiplayerRules::CountPlayers(void)
|
|
{
|
|
g_ts_player_alive_specialists = 0;
|
|
g_ts_player_alive_mercenaries = 0;
|
|
g_ts_player_alive_total = 0;
|
|
g_ts_player_spectator = 0;
|
|
g_ts_player_all = 0;
|
|
|
|
|
|
for (entity eFind = world; (eFind = find(eFind, ::classname, "player"));) {
|
|
if (eFind.health > 0) {
|
|
// TODO - when the time comes for teams.
|
|
/*
|
|
if (eFind.team == TEAM_T) {
|
|
g_cs_alive_t++;
|
|
}...
|
|
*/
|
|
|
|
g_ts_player_alive_total++;
|
|
}else{
|
|
// go ahead and count as a spectator as far as we care.
|
|
g_ts_player_spectator++;
|
|
}
|
|
}
|
|
|
|
// These would be authentic spectators, or those that can't even open the buy menu.
|
|
for (entity eFind = world; (eFind = find(eFind, ::classname, "spectator"));) {
|
|
g_ts_player_spectator++;
|
|
}
|
|
|
|
|
|
g_ts_player_all = g_ts_player_alive_total + g_ts_player_spectator;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
=================
|
|
TimeOut
|
|
|
|
Whenever mp_roundtime was being counted down to 0
|
|
=================
|
|
*/
|
|
void
|
|
TSMultiplayerRules::TimeOut(void)
|
|
{
|
|
RoundOver(FALSE, 0, FALSE);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
=================
|
|
PlayerFindSpawn
|
|
|
|
Recursive function that gets the next spawnpoint
|
|
=================
|
|
*/
|
|
// ...maybe not so recursive now, support for that "no 2 places in a row" behavior
|
|
// (at least that's what I think the complex stuff is below?) can be done later if wanted.
|
|
// ANYWAY, the "t" will be -1 for spectator or no-team, either picks from the same points
|
|
// without a team-based mode being on.
|
|
// oh wait we're in some specific "Rules" class so that's a given. eh.
|
|
// ALSO TODO: I don't think this accounts for any odd cases like other players being in the way
|
|
// of a picked spawn point. Maybe do the box-testing nearby to see if a valid spawn point
|
|
// can be found, if not, re-try in 3 other randomly picked spawn locations and still if not
|
|
// (?), wait 10 seconds and try again. That would be divinely weird though.
|
|
entity
|
|
TSMultiplayerRules::PlayerFindSpawn(float t)
|
|
{
|
|
//TAGGG - question.
|
|
// so... why would TS maps even have separate "start" and "deathmatch" spawns?
|
|
// because forcing the player to spawn at either gives different results,
|
|
// neither is missing. tested in ts_bikini
|
|
// ...hm. Actually free-for-all-only maps, only have "info_player_deathmatch" spawns.
|
|
// Interesting..
|
|
//info_player_start
|
|
//info_player_deathmatch
|
|
|
|
//TAGGG - TODO For team-based mode.
|
|
// Looks like The Mercenaries use info_player_deathmatch.
|
|
// The Specialists use info_player_start.
|
|
|
|
entity eSpot;
|
|
|
|
int randomSpawnChoice = randomInRange_i(0, ary_spawnStart_deathmatch_softMax-1);
|
|
eSpot = ary_spawnStart_deathmatch[randomSpawnChoice];
|
|
|
|
return eSpot;
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
=================
|
|
PlayerRespawn
|
|
|
|
Called whenever a player survived a round and needs a basic respawn.
|
|
=================
|
|
*/
|
|
void
|
|
TSMultiplayerRules::PlayerRespawn(NSClientPlayer pp, int fTeam)
|
|
{
|
|
entity eSpawn;
|
|
vector myOrigin;
|
|
player pl = (player)pp;
|
|
|
|
// should be no need, PlayerRespawn should only be called after MakePlayable calls for the player
|
|
// spawnfunc, that includes setting the classname (even explicitly inside the player constructor)
|
|
//pl.classname = "player";
|
|
|
|
// fresh inventory for you
|
|
TS_resetPlayer(pl, TRUE);
|
|
|
|
// Also, FreeHL did these, but it looks like a simple way of 0'ing everything out.
|
|
// No need with all that 'TS_resetPlayer' does now.
|
|
//LevelNewParms();
|
|
//LevelDecodeParms(pl);
|
|
|
|
// And a Nuclide-call that may not make as much sense with 'UpdateAmmo' across the weapons
|
|
// not being used.
|
|
//Weapons_RefreshAmmo(pl);
|
|
|
|
|
|
forceinfokey(pl, "*spec", "0");
|
|
|
|
pl.health = pl.max_health = 100;
|
|
pl.armor = 0; // may as well enforce that too?
|
|
forceinfokey(pl, "*dead", "0");
|
|
CountPlayers();
|
|
|
|
pl.takedamage = DAMAGE_YES;
|
|
pl.solid = SOLID_SLIDEBOX;
|
|
pl.movetype = MOVETYPE_WALK;
|
|
pl.flags = FL_CLIENT;
|
|
pl.iBleeds = TRUE;
|
|
|
|
// other safe defaults?
|
|
pl.gravity = NULL;
|
|
pl.frame = 1;
|
|
pl.velocity = [0,0,0];
|
|
|
|
pl.SendFlags = UPDATE_ALL;
|
|
// is that a good idea?
|
|
pl.customphysics = Empty;
|
|
pl.SetSize(VEC_HULL_MIN, VEC_HULL_MAX);
|
|
|
|
// TAGGG - don't keep playing death sounds!
|
|
// If we play it from the client instead (doubt it), we need to send a message
|
|
// that tells clientside to do the stopping. I think.
|
|
stopSound(pl, CHAN_VOICE);
|
|
|
|
|
|
eSpawn = PlayerFindSpawn(pl.team);
|
|
|
|
|
|
// final coord to put the player.
|
|
// to be adjusted by the "droptofloor" call further down.
|
|
// Maybe even checks for existing entities (players?) being in the way of spawn,
|
|
// although that probably should've been tested before picking a spawnpoint.
|
|
// If so whatever picks a spawn point should probably come with a spawn point to
|
|
// put the player anyway, including droptofloor.
|
|
|
|
|
|
if(autocvar_debug_spawnpointforced == 1){
|
|
|
|
#ifdef TS_CUSTOM_SPAWN_ORIGIN
|
|
myOrigin = TS_CUSTOM_SPAWN_ORIGIN;
|
|
#else
|
|
printfline("WARNING! debug_spawnpointforced set, but TS_CUSTOM_SPAWN_ORIGIN is not defined!");
|
|
myOrigin = eSpawn.origin;
|
|
#endif
|
|
|
|
#ifdef TS_CUSTOM_SPAWN_VANGLE
|
|
pl.angles = TS_CUSTOM_SPAWN_VANGLE;
|
|
#else
|
|
printfline("WARNING! debug_spawnpointforced set, but TS_CUSTOM_SPAWN_VANGLE is not defined!");
|
|
pl.angles = eSpawn.angles;
|
|
#endif
|
|
}else{
|
|
|
|
myOrigin = eSpawn.origin;
|
|
pl.angles = eSpawn.angles;
|
|
}
|
|
|
|
// Why do we have to do this now? No clue.
|
|
// something about animation.h maybe? I Forget.
|
|
pl.v_angle = pl.angles;
|
|
|
|
|
|
Client_FixAngle(pl, pl.angles);
|
|
|
|
// Because, "droptofloor" needs to be dealing with the up-to-date origin.
|
|
// wait no. I don't think droptofloor is even applying to the player.
|
|
// How do we make it apply to the player, despite not being the most
|
|
// recently spawned entity? Horray for hardcoded behavior.
|
|
// My guess is setting 'self' before-hand, but I don't see what 'droptofloor'
|
|
// could possibly do that a trace below couldn't anyway.
|
|
//pl.SetOrigin(myOrigin);
|
|
|
|
|
|
//if(!droptofloor()){
|
|
vector vSource = myOrigin + [0,0,0];
|
|
traceline ( vSource, vSource + ( '0 0 -1' * 160 ), TRUE, pl );
|
|
|
|
if(trace_fraction < 1.0){
|
|
//hit something? we're there.
|
|
myOrigin[2] = trace_endpos[2] + 0.2 + -pl.mins.z;
|
|
}
|
|
pl.SetOrigin(myOrigin);
|
|
//}
|
|
|
|
// TEST
|
|
//self.angles = [-270, 160, 0];
|
|
//the 1st number actually sets v_angle.x to -89.9 degrees? (record in-game, "getangle" in console)?... oookay.
|
|
//self.v_angle = [-270, 160, 0];
|
|
|
|
//printfline("spawn stats? %.1f,%.1f,%.1f ?%d %s", eSpawn.angles.x, eSpawn.angles.y, eSpawn.angles.z, (eSpawn.target==NULL), eSpawn.target);
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
//TAGGG - CRITICAL, TODO.
|
|
// Support model choice however original TS did, think by some preference.
|
|
// Half-life's multiplayer model picker is probably a good inspiration for that.
|
|
pl.model = "models/player/laurence/laurence.mdl";
|
|
|
|
// FOR REFERENCE: How FreeHL does it. Don't see use of the built-in method 'infokey' very often.
|
|
// Looks like original TS would do something like this (let the 'model' CVar from the player determine
|
|
// model to use), but TS does a check against HLDS models to forbid their use, makes sense as TS
|
|
// demands a few new types of animations involving stunts, unsure if there are any other changes
|
|
// beyond that.
|
|
// ALSO - TS uses model choice to determine team membership, and teams are named after those models
|
|
// in non-team-based modes, however much sense that makes.
|
|
/*
|
|
pl.model = "models/player.mdl";
|
|
string mymodel = infokey(pl, "model");
|
|
|
|
if (mymodel) {
|
|
mymodel = sprintf("models/player/%s/%s.mdl", mymodel, mymodel);
|
|
if (whichpack(mymodel)) {
|
|
pl.model = mymodel;
|
|
}
|
|
}
|
|
// wait why not use pl.SetModel?
|
|
//setmodel(pl, pl.model);
|
|
*/
|
|
|
|
|
|
//TAGGG - TEST. Has to be copied from the valve folder and named this to make sense
|
|
//pl.model = "models/player_valve.mdl";
|
|
|
|
pl.SetModel(pl.model);
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
WriteByte( MSG_MULTICAST, SVC_CGAMEPACKET );
|
|
WriteByte( MSG_MULTICAST, EVENT_TS::SPAWN );
|
|
msg_entity = pl;
|
|
multicast( [0,0,0], MULTICAST_ONE );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void TSMultiplayerRules::MakePlayerInvisible(player pl){
|
|
pl.SetModelindex(0);
|
|
pl.SetMovetype(MOVETYPE_NONE);
|
|
pl.SetSolid(SOLID_NOT);
|
|
pl.takedamage = DAMAGE_NO;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
=================
|
|
PlayerMakePlayable
|
|
|
|
Called whenever need a full-reinit of a player.
|
|
This may be after a player had died or when the game starts for the first time.
|
|
=================
|
|
*/
|
|
|
|
static void
|
|
MakePlayable(entity targ)
|
|
{
|
|
entity oself = self;
|
|
self = targ;
|
|
|
|
// Bots for FreeTS? Not touching that.
|
|
//if (clienttype(targ) != CLIENTTYPE_REAL)
|
|
// spawnfunc_csbot();
|
|
//else
|
|
spawnfunc_player();
|
|
|
|
self = oself;
|
|
}
|
|
|
|
void
|
|
TSMultiplayerRules::PlayerMakePlayable(NSClientPlayer pp)
|
|
{
|
|
player pl = (player)pp;
|
|
|
|
pl.iState = PLAYER_STATE::SPAWNED;
|
|
// Nope!
|
|
//MakePlayable(pp);
|
|
|
|
forceinfokey(pl, "*team", ftos(pl.team));
|
|
PlayerRespawn(pl, pl.team);
|
|
}
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::PlayerMakePlayableWithDefaultMoney(NSClientPlayer pp){
|
|
player pl = (player)pp;
|
|
PlayerMakePlayable(pp);
|
|
setPlayerMoneyDefault(pl);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::PlayerMakeSpectatorAndNotify(NSClientPlayer pp){
|
|
|
|
PlayerMakeSpectator(pp);
|
|
|
|
// let the client know of the chane to revert any 'cl_thirdperson' change
|
|
WriteByte( MSG_MULTICAST, SVC_CGAMEPACKET );
|
|
WriteByte( MSG_MULTICAST, EVENT_TS::PLAYER_NOCLIP );
|
|
msg_entity = pp;
|
|
multicast( [0,0,0], MULTICAST_ONE_R );
|
|
}
|
|
|
|
// Turns the player into a fake spectator (open buymenu and fire to spawn
|
|
// when its closed)
|
|
void
|
|
TSMultiplayerRules::PlayerMakeSpectator(NSClientPlayer pp)
|
|
{
|
|
player pl = (player)pp;
|
|
|
|
if(pl.modelindex != 0){
|
|
// assume this is necessary
|
|
MakePlayerInvisible(pl);
|
|
}
|
|
|
|
pl.iState = PLAYER_STATE::NOCLIP;
|
|
|
|
// And do the rest of the lines to finish that
|
|
// (copied from the Nuclide spectator's constructor)
|
|
// Lines already handled by MakePlayerInvisible not here.
|
|
pl.flags = FL_CLIENT;
|
|
pl.think = NULL;
|
|
pl.nextthink = 0.0f;
|
|
pl.maxspeed = 250;
|
|
|
|
forceinfokey(pl, "*spec", "1");
|
|
|
|
pl.armor = pl.activeweapon = pl.g_items = 0;
|
|
pl.health = 0;
|
|
pl.SetMovetype(MOVETYPE_NOCLIP);
|
|
}
|
|
|
|
|
|
|
|
// Similar to above, but mimicks the FreeCS way of setting the
|
|
// "think" to a tiny method that calls spawnfunc_spectator.
|
|
void
|
|
TSMultiplayerRules::PlayerMakeSpectatorDelayed(NSClientPlayer pp)
|
|
{
|
|
// Nevermind, this has no significance anymore with the client entity change no longer
|
|
// happening. Redirect to the normal version
|
|
player pl = (player)pp;
|
|
PlayerMakeSpectator(pp);
|
|
|
|
}
|
|
|
|
/*
|
|
=================
|
|
PlayerSpawn
|
|
|
|
Called on the client first joining the server.
|
|
=================
|
|
*/
|
|
// NOTE - this is as said above, fist time connecting to a server.
|
|
// NOT spawning ingame with collision, weapon viewmodel, seen by other players, etc.
|
|
void
|
|
TSMultiplayerRules::PlayerSpawn(NSClientPlayer pp)
|
|
{
|
|
player pl = (player)pp;
|
|
|
|
//printfline("PlayerSpawn, what is classname before anything has been done? %s %s", self.classname, pp.classname);
|
|
|
|
// Apparently the player already arrives as a "player", a Nuclide-implemented event called
|
|
// ClientConnect makes the client entity a "player" by using spawnfunc.
|
|
// Having the "player" class available that early makes me wonder why "NSClientPlayer" is used so
|
|
// often in implemented methods, but not an issue.
|
|
// ClientConnect is called before PutClientInServer (the one that leads to the gamerules
|
|
// PlayerSpawn, right here), just for info.
|
|
|
|
// What this means is, no need for spawnfunc_player here.
|
|
|
|
|
|
// should "Frags" be an infokey to be better preserved through player/spectator changes?
|
|
// No clue. And setting these only that way further down by forceinfokey too.
|
|
//pl.frags = 0;
|
|
//pl.deaths = 0;
|
|
//pl.team = TEAM_SPECTATOR;
|
|
|
|
|
|
// immediately put us into fake spectating mode (MoTD followed by buy-menu)
|
|
PlayerMakeSpectator(pl);
|
|
// Use our game's custom ObserverCam instead.
|
|
Game_Spawn_ObserverCam(pl);
|
|
|
|
// give the initial server-joining money
|
|
setPlayerMoneyDefault(pl);
|
|
|
|
// I guess this state counts as "dead"?
|
|
forceinfokey(pl, "*dead", "1");
|
|
|
|
// we don't belong to any team
|
|
forceinfokey(pl, "*team", "0");
|
|
forceinfokey(pl, "*deaths", "0"); //ftos(pl.deaths));
|
|
|
|
forceinfokey(pl, "done_connecting", "1");
|
|
|
|
// ALSO - start the minimum spawn delay.
|
|
pl.minimumRespawnTime = autocvar_respawntime;
|
|
|
|
}
|
|
|
|
|
|
void
|
|
TSMultiplayerRules::setPlayerMoneyDefault(player pl)
|
|
{
|
|
pl.money = 0;
|
|
Money_AddMoney(pl, autocvar_mp_startmoney);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CSEv_GameEarlyNoclip(void)
|
|
{
|
|
TSMultiplayerRules rules;
|
|
player pl = (player)self;
|
|
|
|
if(pl.iState != PLAYER_STATE::DEAD_RECENT){
|
|
// This is for looking at a recently dead player only.
|
|
return;
|
|
}
|
|
|
|
if(pl.deathCameraChangeTime > 1.5){
|
|
if(pl.deathCameraChangeTime < 1.5 + 0.5){
|
|
pl.waitingForEarlyNoclip = TRUE;
|
|
}
|
|
return;
|
|
}
|
|
|
|
rules = (TSMultiplayerRules)g_grMode;
|
|
rules.PlayerMakeSpectatorAndNotify(pl);
|
|
|
|
|
|
}
|
|
|
|
// An order from the client (while in spectator) that they want to spawn.
|
|
void
|
|
CSEv_GamePlayerSpawn(void)
|
|
{
|
|
TSMultiplayerRules rules;
|
|
player pl = (player)self;
|
|
|
|
if(pl.iState == PLAYER_STATE::SPAWNED){
|
|
// already spawned?
|
|
return;
|
|
}
|
|
|
|
if(pl.minimumRespawnTime > 0){
|
|
// the spawn-delay hasn't expired yet, then no.
|
|
// Although, if close enough, mark this as 'waitingToSpawn' to happen as it finishes
|
|
if(pl.minimumRespawnTime < 0.5){
|
|
pl.waitingForSpawn = TRUE;
|
|
}
|
|
return;
|
|
}
|
|
|
|
rules = (TSMultiplayerRules)g_grMode;
|
|
rules.PlayerMakePlayableWithDefaultMoney(pl);
|
|
}
|