L4D2 Level Design/Custom Finale
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 with arbitrary conditions (mainly the onslaught stage type). Something elaborate as a custom boss is possible, for example, where a certain amount of damage advances the finale to the next stage. Once the stages listed are finished, trigger_finale fires a FinaleEscapeStarted output.
None of the official L4D2 maps use the Standard
finale option. 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).
Components
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("========================================================");
}
}
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.
Sacrifice finale
The sacrifice finale is based off of custom finale with additional hard-coded modifications introduced in The Sacrifice update.
← L4D2 Level Design/Gauntlet Finale | Return to L4D2 Level Design |