L4D2 Level Design/Custom Finale

From Valve Developer Community
Jump to: navigation, search

<Left 4 Dead 2> A custom finale reads automatically off of a vscript containing a list of stages, <map name>_finale.nut. The system features the ability to increment finale stages when arbitrary conditions are met (i.e. boss monster, Aztec tomb puzzles, feats of strength, etc.). Once all stages are finished, trigger_finale fires a FinaleEscapeStarted output.

Custom finale maps include c2m5_concert, c3m4_plantation, c4m5_milltown_escape, c7m3_port, and it is assumed that L4D1 finales do the same (No Mercy is confirmed to use custom). Official maps do not use the Standard finale type.

Components

Map

At the very least, all that needs to be changed is trigger_finale Finale Type, from Standard to Custom. There are other options and details you should consider:

  • The onslaught stage type does not end unless the director is given the input EndCustomScriptedStage via script EntFire (direct or indirect) or simply in-game I/O.
  • info_director: OnCustomPanicStageFinished, OnPanicEventFinished (maybe just for crescendo), and OnUserDefinedScriptEvent(1-4) outputs are available, linked to vscript stage states or methods such as .UserDefinedEvent1()-.UserDefinedEvent4().
  • trigger_finale: AdvanceFinaleState input is available.

VScript

As discussed in the L4D2 vscript article, there are four stage types, additional custom finale-specific director options, and special functions available.

The following are custom finale vscripts with additional comments:

c2m5_concert_finale.nut

//-----------------------------------------------------------------------------
// Enumerations of stage types

ERROR <- -1 //This enumeration is not always used, but what the heck!
PANIC <- 0
TANK <- 1
DELAY <- 2
ONSLAUGHT <- 3

//-----------------------------------------------------------------------------
// Initialization of tables that will be fed to DirectorOptions

SharedOptions <-
{
	A_CustomFinale_StageCount = 9 //Number of stages. Used by director VS scoring, as well??
	
 	A_CustomFinale1 = PANIC
	A_CustomFinaleValue1 = 1 //1 PANIC waves
	
 	A_CustomFinale2 = PANIC
	A_CustomFinaleValue2 = 1

	A_CustomFinale3 = DELAY
	A_CustomFinaleValue3 = 15 //15 seconds of DELAY

	A_CustomFinale4 = TANK
	A_CustomFinaleValue4 = 1 //1 TANK
	A_CustomFinaleMusic4 = "" //Custom music entry is off in script, played in-game

	A_CustomFinale5 = DELAY
	A_CustomFinaleValue5 = 15

	A_CustomFinale6 = PANIC
	A_CustomFinaleValue6 = 2

	A_CustomFinale7 = DELAY
	A_CustomFinaleValue7 = 10

	A_CustomFinale8 = TANK
	A_CustomFinaleValue8 = 1
	A_CustomFinaleMusic8 = ""

	A_CustomFinale9 = DELAY
	A_CustomFinaleValue9 = RandomInt( 5, 10 ) //Random DELAY between 5-10 seconds
	
        // Additional Director options
	PreferredMobDirection = SPAWN_LARGE_VOLUME
	PreferredSpecialDirection = SPAWN_LARGE_VOLUME
	ShouldConstrainLargeVolumeSpawn = false

	ZombieSpawnRange = 3000
	
	SpecialRespawnInterval = 20
} 

InitialPanicOptions <- //Table separate from SharedOptions for stage 1
{
	ShouldConstrainLargeVolumeSpawn = true
}


PanicOptions <- //General panic options
{
	CommonLimit = 25
}

TankOptions <- //Another separate table used when TANK in play
{
	ShouldAllowSpecialsWithTank = true
	SpecialRespawnInterval = 30
}


DirectorOptions <- clone SharedOptions //DirectorOptions starts off with SharedOptions
{
}

//-----------------------------------------------------------------------------
// Used frequently to copy table to another one

function AddTableToTable( dest, src )
{
	foreach( key, val in src )
	{
		dest[key] <- val
	}
}

//-----------------------------------------------------------------------------
// Manipulation of DirectorOptions with custom logic
// In this case, DirectorOptions only changes when a new stage starts

function OnBeginCustomFinaleStage( num, type ) //Special func, when every new finale stage begins
{
	if ( developer() > 0 ) //If developer mode is on, -dev
	{
		printl("========================================================");
		printl( "Beginning custom finale stage " + num + " of type " + type );
	}

        //Setting up / determining WAVEOPTIONS   
	local waveOptions = null
	if ( num == 1 ) //If first stage (assumed to be PANIC)
	{
		waveOptions = InitialPanicOptions
	}
	else if ( type == PANIC ) //General PANIC
	{
		waveOptions = PanicOptions

                /* Change MegaMobSize if MegaMobMinSize is available in PanicOptions is available.
                   Was this ever used?? */
		if ( "MegaMobMinSize" in PanicOptions )
		{
			waveOptions.MegaMobSize <- RandomInt( PanicOptions.MegaMobMinSize, MegaMobMaxSize )
		}
	}
	else if ( type == TANK ) //TANK time!
	{
		waveOptions = TankOptions
	}
	
	//---------------------------------
        // Done determining WAVEOPTIONS. Now, actually move to DirectorOptions

	MapScript.DirectorOptions.clear() //Clear all DirectorOptions

	AddTableToTable( MapScript.DirectorOptions, SharedOptions ); //Bring back SharedOptions

	if ( waveOptions != null ) //Finally add the stage-dependent options (WAVEOPTIONS)
	{
		AddTableToTable( MapScript.DirectorOptions, waveOptions );
	}

	//---------------------------------

	if ( developer() > 0 ) //More dev outputs (-dev)
	{
		Msg( "\n*****\nMapScript.DirectorOptions:\n" );
		foreach( key, value in MapScript.DirectorOptions )
		{
			Msg( "    " + key + " = " + value + "\n" );
		}

		if ( LocalScript.rawin( "DirectorOptions" ) ) //RAWIN checks if DirectorOptions exists
		{
			Msg( "\n*****\nLocalScript.DirectorOptions:\n" );
			foreach( key, value in LocalScript.DirectorOptions )
			{
				Msg( "    " + key + " = " + value + "\n" );
			}
		}
		printl("========================================================");
	}
}

Sacrifice finale

The sacrifice finale is based off of custom finale with additional hard-coded modifications introduced in The Sacrifice update.

To do: break it down, explain, tutorialize

c7m3_port_finale.nut

This is more complex than other custom finales but essentially does so to customize the experience. The script suggests that Sacrifice finale types depend mostly on new in-game entities and entity features.

//-----------------------------------------------------
// This script handles the logic for the Port / Bridge
// finale in the River Campaign. 
//
//-----------------------------------------------------
Msg("Initiating c7m3_port_finale script\n");

//-----------------------------------------------------
// Enumerations
ERROR		<- -1
PANIC 		<- 0
TANK 		<- 1
DELAY 		<- 2

//-----------------------------------------------------

// This keeps track of the number of times the generator button has been pressed. 
// Init to 1, since one button press has been used to start the finale and run 
// this script. 
ButtonPressCount <- 1

// This stores the stage number that we last
// played the "Press the Button!" VO
LastVOButtonStageNumber <- 0

// We use this to keep from running a bunch of queued advances really quickly. 
// Init to true because we are starting a finale from a button press in the pre-finale script 
// see GeneratorButtonPressed in c7m3_port.nut
PendingWaitAdvance <- true	

// We use three generator button presses to push through
// 8 stages. We have to queue up state advances
// depending on the state of the finale when buttons are pressed
QueuedDelayAdvances <- 0


// Tracking current finale states
CurrentFinaleStageNumber <- ERROR
CurrentFinaleStageType <- ERROR

// The finale is 3 phases. 
// We randomize the event types in the first two
local RandomFinaleStage1 = 0
local RandomFinaleStage2 = 0
local RandomFinaleStage4 = 0
local RandomFinaleStage5 = 0

// PHASE 1 EVENTS
if ( RandomInt( 1, 100 ) < 50 )
{
	RandomFinaleStage1 = PANIC
	RandomFinaleStage2 = TANK
}
else
{
	RandomFinaleStage1 = TANK
	RandomFinaleStage2 = PANIC
}


// PHASE 2 EVENTS
if ( RandomInt( 1, 100 ) < 50 )
{
	RandomFinaleStage4 = PANIC
	RandomFinaleStage5 = TANK
}
else
{
	RandomFinaleStage4 = TANK
	RandomFinaleStage5 = PANIC
}



// We want to give the survivors a little of extra time to 
// get on their feet before the escape, since you have to fight through 
// the sacrifice.

PreEscapeDelay <- 0
if ( Director.GetGameMode() == "coop" )
{
	PreEscapeDelay <- 5
}
else if ( Director.GetGameMode() == "versus" )
{
	PreEscapeDelay <- 15
}

DirectorOptions <-
{	
	 
	A_CustomFinale_StageCount = 8
	 
	// PHASE 1
	A_CustomFinale1 = RandomFinaleStage1
	A_CustomFinaleValue1 = 1
	A_CustomFinale2 = RandomFinaleStage2
	A_CustomFinaleValue2 = 1
	A_CustomFinale3 = DELAY
	A_CustomFinaleValue3 = 9999
	
	
	// PHASE 2
	A_CustomFinale4 = RandomFinaleStage4
	A_CustomFinaleValue4 = 1
	A_CustomFinale5 = RandomFinaleStage5
	A_CustomFinaleValue5 = 1	
	A_CustomFinale6 = DELAY
	A_CustomFinaleValue6 = 9999 	 
	
	
	// PHASE 3
	A_CustomFinale7 = TANK
	A_CustomFinaleValue7 = 1	 	 		 
	A_CustomFinale8 = DELAY
	A_CustomFinaleValue8 = PreEscapeDelay
	 
	 
	 
	TankLimit = 4
	WitchLimit = 0
	CommonLimit = 20	
	HordeEscapeCommonLimit = 15	
	EscapeSpawnTanks = false
	//SpecialRespawnInterval = 80

}


function OnBeginCustomFinaleStage( num, type )
{
	printl( "*!* Beginning custom finale stage " + num + " of type " + type );
	printl( "*!* PendingWaitAdvance " + PendingWaitAdvance + ", QueuedDelayAdvances " + QueuedDelayAdvances );
	
	// Store off the state... 
	CurrentFinaleStageNumber = num
	CurrentFinaleStageType = type
	
	// Acknowledge the state advance
	PendingWaitAdvance = false
}


function GeneratorButtonPressed()
{
    printl( "*!* GeneratorButtonPressed finale stage " + CurrentFinaleStageNumber + " of type " +CurrentFinaleStageType );
	printl( "*!* PendingWaitAdvance " + PendingWaitAdvance + ", QueuedDelayAdvances " + QueuedDelayAdvances );
	
	
	ButtonPressCount++
	
	
	local ImmediateAdvances = 0
	
	
	if ( CurrentFinaleStageNumber == 1 || CurrentFinaleStageNumber == 4 )
	{		
		// First stage of a phase, so next stage is an "action" stage too.
		// Advance to next action stage, and then queue an advance to the 
		// next delay.
		QueuedDelayAdvances++
		ImmediateAdvances = 1
	}
	else if ( CurrentFinaleStageNumber == 2 || CurrentFinaleStageNumber == 5 )
	{
		// Second stage of a phase, so next stage is a "delay" stage.
		// We need to immediately advance past the delay and into an action state. 
		
		//QueuedDelayAdvances++	// NOPE!
		ImmediateAdvances = 2
	}
	else if ( CurrentFinaleStageNumber == 3 || CurrentFinaleStageNumber == 6 )
	{
		// Wait states... (very long delay)
		// Advance immediately into an action state
		
		//QueuedDelayAdvances++
		ImmediateAdvances = 1
	}
	else if ( CurrentFinaleStageNumber == -1 || 
              CurrentFinaleStageNumber == 0 )
	{
		// the finale is *just* about to start... 
		// we can get this if all the buttons are hit at once at the beginning
		// Just queue a wait advance
		QueuedDelayAdvances++
		ImmediateAdvances = 0
	}
	else
	{
		printl( "*!* Unhandled generator button press! " );
	}

	if ( ImmediateAdvances > 0 )
	{	
		EntFire( "generator_start_model", "Enable" )
		
		
		if ( ImmediateAdvances == 1 )
		{
			printl( "*!* GeneratorButtonPressed Advancing State ONCE");
			EntFire( "generator_start_model", "AdvanceFinaleState" )
		}
		else if ( ImmediateAdvances == 2 )
		{
			printl( "*!* GeneratorButtonPressed Advancing State TWICE");
			EntFire( "generator_start_model", "AdvanceFinaleState" )
			EntFire( "generator_start_model", "AdvanceFinaleState" )
		}
		
		EntFire( "generator_start_model", "Disable" )
		
		PendingWaitAdvance = true
	}
	
}

function Update() //Called every 0.100 seconds
{
	// Should we advance the finale state?
	// 1. If we are in a DELAY state
	// 2. And we have queued advances.... 
	// 3. And we have not just tried to advance the advance the state.... 
	if ( CurrentFinaleStageType == DELAY && QueuedDelayAdvances > 0 && !PendingWaitAdvance )
	{
		// If things are calm (relatively), jump to the next state
		if ( !Director.IsTankInPlay() && !Director.IsAnySurvivorInCombat() )
		{
			if ( Director.GetPendingMobCount() < 1 && Director.GetCommonInfectedCount() < 5 )
			{
				printl( "*!* Update Advancing State finale stage " + CurrentFinaleStageNumber + " of type " +CurrentFinaleStageType );
				printl( "*!* PendingWaitAdvance " + PendingWaitAdvance + ", QueuedDelayAdvances " + QueuedDelayAdvances );
		
				QueuedDelayAdvances--
				EntFire( "generator_start_model", "Enable" )
				EntFire( "generator_start_model", "AdvanceFinaleState" )
				EntFire( "generator_start_model", "Disable" )
				PendingWaitAdvance = true
			}
		}
	}
	
	// Should we fire the director event to play the "Press the button!" Nag VO?	
	// If we are on an infinite delay stage...
	if ( CurrentFinaleStageType == DELAY && CurrentFinaleStageNumber > 1 && CurrentFinaleStageNumber < 7 )	
	{		
		// 1. We have not nagged for this stage yet
		// 2. There are button presses remaining
		if ( CurrentFinaleStageNumber != LastVOButtonStageNumber && ButtonPressCount < 3 )
		{
			// We are not about to process a wait advance..
			if ( QueuedDelayAdvances == 0 && !PendingWaitAdvance )
			{
				// If things are pretty calm, run the event
				if ( Director.GetPendingMobCount() < 1 && Director.GetCommonInfectedCount() < 1 )
				{
					if ( !Director.IsTankInPlay() && !Director.IsAnySurvivorInCombat() )
					{
						printl( "*!* Update firing event 1 (VO Prompt)" )
						LastVOButtonStageNumber = CurrentFinaleStageNumber
						Director.UserDefinedEvent1()
					}
				}
			}
		}
	}
	
}


function EnableEscapeTanks()
// This is called in-game via logic_relay at info_director
// enable the escape tanks at a different time.
// Input: Runscriptcode
// Parm: DirectorScript.MapScript.LocalScript.EnableEscapeTanks()
{
	printl( "*!* EnableEscapeTanks finale stage " + CurrentFinaleStageNumber + " of type " +CurrentFinaleStageType );
	
	//Msg( "\n*****\nMapScript.DirectorOptions:\n" );
	//foreach( key, value in MapScript.DirectorOptions )
	//{
	//	Msg( "    " + key + " = " + value + "\n" );
	//}

	MapScript.DirectorOptions.EscapeSpawnTanks <- true
}
L4D2 Level Design/Gauntlet Finale Return to L4D2 Level Design