diff --git a/userraw/maps/mp/gametypes/_killcam.gsc b/userraw/maps/mp/gametypes/_killcam.gsc new file mode 100644 index 0000000..1a232d6 --- /dev/null +++ b/userraw/maps/mp/gametypes/_killcam.gsc @@ -0,0 +1,594 @@ +#include maps\mp\_utility; +#include maps\mp\gametypes\_hud_util; + +init() +{ + precacheString(&"PLATFORM_PRESS_TO_SKIP"); + precacheString(&"PLATFORM_PRESS_TO_RESPAWN"); + precacheString(&"PLATFORM_PRESS_TO_COPYCAT"); + precacheShader("specialty_copycat"); + + level.killcam = maps\mp\gametypes\_tweakables::getTweakableValue( "game", "allowkillcam" ); +} + +killcam( + attackerNum, // entity number of the attacker + killcamentityindex, // entity number of the entity to view (grenade, airstrike, etc) + killcamentitystarttime, // time at which the killcamentity came into being + sWeapon, // killing weapon + predelay, // time between player death and beginning of killcam + offsetTime, // something to do with how far back in time the killer was seeing the world when he made the kill; latency related, sorta + timeUntilRespawn, // will the player be allowed to respawn after the killcam? + maxtime, // time remaining until map ends; the killcam will never last longer than this. undefined = no limit + attacker, // entity object of attacker + victim // entity object of the victim +) +{ + // monitors killcam and hides HUD elements during killcam session + //if ( !level.splitscreen ) + // self thread killcam_HUD_off(); + + self endon("disconnect"); + self endon("spawned"); + level endon("game_ended"); + + if ( attackerNum < 0 ) + return; + + // length from killcam start to killcam end + if (getdvar("scr_killcam_time") == "") { + if ( sWeapon == "artillery_mp" || sWeapon == "stealth_bomb_mp" ) + camtime = (gettime() - killcamentitystarttime) / 1000 - predelay - .1; + else if ( level.showingFinalKillcam ) + camtime = 4.0; + else if ( sWeapon == "javelin_mp" ) + camtime = 8; + else if ( issubstr( sWeapon, "remotemissile_" ) ) + camtime = 5; + else if ( !timeUntilRespawn || timeUntilRespawn > 5.0 ) // if we're not going to respawn, we can take more time to watch what happened + camtime = 5.0; + else if ( sWeapon == "frag_grenade_mp" || sWeapon == "frag_grenade_short_mp" || sWeapon == "semtex_mp" ) + camtime = 4.25; // show long enough to see grenade thrown + else + camtime = 2.5; + } + else + camtime = getdvarfloat("scr_killcam_time"); + + if (isdefined(maxtime)) { + if (camtime > maxtime) + camtime = maxtime; + if (camtime < .05) + camtime = .05; + } + + // time after player death that killcam continues for + if (getdvar("scr_killcam_posttime") == "") + postdelay = 2; + else { + postdelay = getdvarfloat("scr_killcam_posttime"); + if (postdelay < 0.05) + postdelay = 0.05; + } + + /* timeline: + + | camtime | postdelay | + | | predelay | + + ^ killcam start ^ player death ^ killcam end + ^ player starts watching killcam + + */ + + killcamlength = camtime + postdelay; + + // don't let the killcam last past the end of the round. + if (isdefined(maxtime) && killcamlength > maxtime) + { + // first trim postdelay down to a minimum of 1 second. + // if that doesn't make it short enough, trim camtime down to a minimum of 1 second. + // if that's still not short enough, cancel the killcam. + if ( maxtime < 2 ) + return; + + if (maxtime - camtime >= 1) { + // reduce postdelay so killcam ends at end of match + postdelay = maxtime - camtime; + } + else { + // distribute remaining time over postdelay and camtime + postdelay = 1; + camtime = maxtime - 1; + } + + // recalc killcamlength + killcamlength = camtime + postdelay; + } + + killcamoffset = camtime + predelay; + + startTime = getTime(); + self notify ( "begin_killcam", startTime ); + + self.sessionstate = "spectator"; + self.forcespectatorclient = attackerNum; + self.killcamentity = -1; + if ( killcamentityindex >= 0 ) + self thread setKillCamEntity( killcamentityindex, killcamoffset, killcamentitystarttime ); + self.archivetime = killcamoffset; + self.killcamlength = killcamlength; + self.psoffsettime = offsetTime; + + // ignore spectate permissions + self allowSpectateTeam("allies", true); + self allowSpectateTeam("axis", true); + self allowSpectateTeam("freelook", true); + self allowSpectateTeam("none", true); + + if ( isDefined( attacker ) && level.showingFinalKillcam ) // attacker may have disconnected + { + self openMenu( "killedby_card_display" ); + self SetCardDisplaySlot( attacker, 7 ); + } + + self thread endedKillcamCleanup(); + + // wait till the next server frame to allow code a chance to update archivetime if it needs trimming + wait 0.05; + + assertex( self.archivetime <= killcamoffset + 0.0001, "archivetime: " + self.archivetime + ", killcamoffset: " + killcamoffset ); + if ( self.archivetime < killcamoffset ) + println( "WARNING: Code trimmed killcam time by " + (killcamoffset - self.archivetime) + " seconds because it doesn't have enough game time recorded!" ); + + camtime = self.archivetime - .05 - predelay; + killcamlength = camtime + postdelay; + self.killcamlength = killcamlength; + + if ( camtime <= 0 ) // if we're not looking back in time far enough to even see the death, cancel + { + println( "Cancelling killcam because we don't even have enough recorded to show the death." ); + + self.sessionstate = "dead"; + self.forcespectatorclient = -1; + self.killcamentity = -1; + self.archivetime = 0; + self.psoffsettime = 0; + + self notify ( "killcam_ended" ); + + return; + } + + if ( level.showingFinalKillcam ) + thread doFinalKillCamFX( camtime ); + + self.killcam = true; + + self initKCElements(); + + if ( !level.splitscreen ) + { + self.kc_timer.alpha = 1; + self.kc_timer setTenthsTimer(camtime); + } + + if ( timeUntilRespawn && !level.gameEnded ) + { + if ( timeUntilRespawn > 0 ) + setLowerMessage( "kc_info", game["strings"]["waiting_to_spawn"], timeUntilRespawn ); + else + setLowerMessage( "kc_info", &"PLATFORM_PRESS_TO_SKIP" ); + } + else if ( !level.gameEnded ) + { + setLowerMessage( "kc_info", &"PLATFORM_PRESS_TO_RESPAWN" ); + } + + if ( !level.showingFinalKillcam ) + self.kc_skiptext.alpha = 1; + else + self.kc_skiptext.alpha = 0; + + self.kc_othertext.alpha = 0; + self.kc_icon.alpha = 0; + + self thread spawnedKillcamCleanup(); + + if ( self == victim && victim _hasPerk( "specialty_copycat" ) && isDefined( victim.pers["copyCatLoadout"] ) ) + self thread waitKCCopyCatButton( attacker ); + + if ( !level.showingFinalKillcam ) + self thread waitSkipKillcamButton( timeUntilRespawn ); + else + self notify ( "showing_final_killcam" ); + + self thread endKillcamIfNothingToShow(); + + self waittillKillcamOver(); + + if ( level.showingFinalKillcam ) + { + self thread maps\mp\gametypes\_playerlogic::spawnEndOfGame(); + return; + } + + self thread calculateKillCamTime( startTime ); + + self thread killcamCleanup( true ); +} + + +doFinalKillCamFX( camTime ) +{ + if ( isDefined( level.doingFinalKillcamFx ) ) + return; + level.doingFinalKillcamFx = true; + + intoSlowMoTime = camTime; + if ( intoSlowMoTime > 1.0 ) + { + intoSlowMoTime = 1.0; + wait( camTime - 1.0 ); + } + + setSlowMotion( 1.0, 0.25, intoSlowMoTime ); // start timescale, end timescale, lerp duration + wait( intoSlowMoTime + .5 ); + setSlowMotion( 0.25, 1, 1.0 ); + + level.doingFinalKillcamFx = undefined; +} + + +calculateKillCamTime( startTime ) +{ + watchedTime = int(getTime() - startTime); + self incPlayerStat( "killcamtimewatched", watchedTime ); +} + +waittillKillcamOver() +{ + self endon("abort_killcam"); + + wait(self.killcamlength - 0.05); +} + +setKillCamEntity( killcamentityindex, killcamoffset, starttime ) +{ + self endon("disconnect"); + self endon("killcam_ended"); + + killcamtime = (gettime() - killcamoffset * 1000); + + if ( starttime > killcamtime ) + { + wait .05; + // code may have trimmed archivetime after the first frame if we couldn't go back in time as far as requested. + killcamoffset = self.archivetime; + killcamtime = (gettime() - killcamoffset * 1000); + + if ( starttime > killcamtime ) + wait (starttime - killcamtime) / 1000; + } + self.killcamentity = killcamentityindex; +} + +waitSkipKillcamButton( timeUntilRespawn ) +{ + self endon("disconnect"); + self endon("killcam_ended"); + + while(self useButtonPressed()) + wait .05; + + while(!(self useButtonPressed())) + wait .05; + + if ( !matchMakingGame() ) + self incPlayerStat( "killcamskipped", 1 ); + + if ( timeUntilRespawn <= 0 ) + clearLowerMessage( "kc_info" ); + + self notify("abort_killcam"); +} + +waitKCCopyCatButton( attacker ) +{ + self endon("disconnect"); + self endon("killcam_ended"); + + self waitCopyCatButton( attacker ); + + self notify("abort_killcam"); +} + +waitDeathCopyCatButton( attacker ) +{ + self endon ( "disconnect" ); + + self initKCElements(); + + usedCopycat = self waitCopyCatButton( attacker ); + + if ( !isDefined( usedCopycat ) ) + { + self.kc_icon.alpha = 0; + self.kc_othertext.alpha = 0; + } +} + +waitCopyCatButton( attacker ) +{ + self endon ( "spawned_player" ); + self endon ( "death_delay_finished" ); + self.kc_icon setShader( "specialty_copycat", 48, 48 ); + self.kc_othertext setText( &"PLATFORM_PRESS_TO_COPYCAT" ); + self.kc_othertext.alpha = 1; + self.kc_icon.alpha = 1; + + self notifyOnPlayerCommand( "use_copycat", "weapnext" ); + + self waittill( "use_copycat" ); + + self.pers["copyCatLoadout"]["inUse"] = true; + self.pers["copyCatLoadout"]["owner"] = attacker; + + self.kc_othertext fadeOverTime( 0.5 ); + self.kc_othertext.alpha = 0; + + self.kc_icon fadeOverTime( 0.25 ); + self.kc_icon scaleOverTime( 0.25, 512, 512 ); + self.kc_icon.alpha = 0; + + if ( isDefined( attacker ) ) + attacker thread maps\mp\gametypes\_hud_message::playerCardSplashNotify( "copied", self ); + + self playLocalSound( "copycat_steal_class" ); + + return true; +} + +waitSkipKillcamSafeSpawnButton() +{ + self endon("disconnect"); + self endon("killcam_ended"); + + if ( !self maps\mp\gametypes\_playerlogic::maySpawn() ) + return; + + while(self fragButtonPressed()) + wait .05; + + while(!(self fragButtonPressed())) + wait .05; + + self.wantSafeSpawn = true; + + self notify("abort_killcam"); +} + +endKillcamIfNothingToShow() +{ + self endon("disconnect"); + self endon("killcam_ended"); + + while(1) + { + // code may trim our archivetime to zero if there is nothing "recorded" to show. + // this can happen when the person we're watching in our killcam goes into killcam himself. + // in this case, end the killcam. + if ( self.archivetime <= 0 ) + break; + wait .05; + } + + self notify("abort_killcam"); +} + +spawnedKillcamCleanup() +{ + self endon("disconnect"); + self endon("killcam_ended"); + + self waittill("spawned"); + self thread killcamCleanup( false ); +} + +endedKillcamCleanup() +{ + self endon("disconnect"); + self endon("killcam_ended"); + + level waittill("game_ended"); + + self thread killcamCleanup( true ); +} + +killcamCleanup( clearState ) +{ + if(isDefined(self.kc_skiptext)) + self.kc_skiptext.alpha = 0; + + if(isDefined(self.kc_timer)) + self.kc_timer.alpha = 0; + + if(isDefined(self.kc_icon)) + self.kc_icon.alpha = 0; + + if(isDefined(self.kc_othertext)) + self.kc_othertext.alpha = 0; + + self.killcam = undefined; + + if ( !level.gameEnded ) + self clearLowerMessage( "kc_info" ); + + self thread maps\mp\gametypes\_spectating::setSpectatePermissions(); + + self notify("killcam_ended"); // do this last, in case this function was called from a thread ending on it + + if ( !clearState ) + return; + + self.sessionstate = "dead"; + self ClearKillcamState(); +} + + + +cancelKillCamOnUse() +{ + self.cancelKillcam = false; + self thread cancelKillCamOnUse_specificButton( ::cancelKillCamUseButton, ::cancelKillCamCallback ); + //self thread cancelKillCamOnUse_specificButton( ::cancelKillCamSafeSpawnButton, ::cancelKillCamSafeSpawnCallback ); +} + +cancelKillCamUseButton() +{ + return self useButtonPressed(); +} +cancelKillCamSafeSpawnButton() +{ + return self fragButtonPressed(); +} +cancelKillCamCallback() +{ + self.cancelKillcam = true; +} +cancelKillCamSafeSpawnCallback() +{ + self.cancelKillcam = true; + self.wantSafeSpawn = true; +} + +cancelKillCamOnUse_specificButton( pressingButtonFunc, finishedFunc ) +{ + self endon ( "death_delay_finished" ); + self endon ( "disconnect" ); + level endon ( "game_ended" ); + + for ( ;; ) + { + if ( !self [[pressingButtonFunc]]() ) + { + wait ( 0.05 ); + continue; + } + + buttonTime = 0; + while( self [[pressingButtonFunc]]() ) + { + buttonTime += 0.05; + wait ( 0.05 ); + } + + if ( buttonTime >= 0.5 ) + continue; + + buttonTime = 0; + + while ( !self [[pressingButtonFunc]]() && buttonTime < 0.5 ) + { + buttonTime += 0.05; + wait ( 0.05 ); + } + + if ( buttonTime >= 0.5 ) + continue; + + self [[finishedFunc]](); + return; + } +} + +initKCElements() +{ + if ( !isDefined( self.kc_skiptext ) ) + { + self.kc_skiptext = newClientHudElem(self); + self.kc_skiptext.archived = false; + self.kc_skiptext.x = 0; + self.kc_skiptext.alignX = "center"; + self.kc_skiptext.alignY = "top"; + self.kc_skiptext.horzAlign = "center_adjustable"; + self.kc_skiptext.vertAlign = "top_adjustable"; + self.kc_skiptext.sort = 1; // force to draw after the bars + self.kc_skiptext.font = "default"; + self.kc_skiptext.foreground = true; + self.kc_skiptext.hideWhenInMenu = true; + + if ( level.splitscreen ) + { + self.kc_skiptext.y = 20; + self.kc_skiptext.fontscale = 1.2; // 1.8/1.5 + } + else + { + self.kc_skiptext.y = 32; + self.kc_skiptext.fontscale = 1.8; + } + } + + if ( !isDefined( self.kc_othertext ) ) + { + self.kc_othertext = newClientHudElem(self); + self.kc_othertext.archived = false; + self.kc_othertext.y = 18; + self.kc_othertext.alignX = "left"; + self.kc_othertext.alignY = "top"; + self.kc_othertext.horzAlign = "center"; + self.kc_othertext.vertAlign = "middle"; + self.kc_othertext.sort = 10; // force to draw after the bars + self.kc_othertext.font = "small"; + self.kc_othertext.foreground = true; + self.kc_othertext.hideWhenInMenu = true; + + if ( level.splitscreen ) + { + self.kc_othertext.x = 16; + self.kc_othertext.fontscale = 1.2; + } + else + { + self.kc_othertext.x = 62; + self.kc_othertext.fontscale = 1.6; + } + } + + if ( !isDefined( self.kc_icon ) ) + { + self.kc_icon = newClientHudElem(self); + self.kc_icon.archived = false; + self.kc_icon.x = 16; + self.kc_icon.y = 16; + self.kc_icon.alignX = "left"; + self.kc_icon.alignY = "top"; + self.kc_icon.horzAlign = "center"; + self.kc_icon.vertAlign = "middle"; + self.kc_icon.sort = 1; // force to draw after the bars + self.kc_icon.foreground = true; + self.kc_icon.hideWhenInMenu = true; + } + + if ( !level.splitscreen ) + { + if ( !isdefined( self.kc_timer ) ) + { + self.kc_timer = createFontString( "hudbig", 1.0 ); + self.kc_timer.archived = false; + self.kc_timer.x = 0; + self.kc_timer.alignX = "center"; + self.kc_timer.alignY = "middle"; + self.kc_timer.horzAlign = "center_safearea"; + self.kc_timer.vertAlign = "top_adjustable"; + self.kc_timer.y = 42; + self.kc_timer.sort = 1; // force to draw after the bars + self.kc_timer.font = "hudbig"; + self.kc_timer.foreground = true; + self.kc_timer.color = (0.85,0.85,0.85); + self.kc_timer.hideWhenInMenu = true; + } + } +} +