Manager.Spawning.cs, part of the game Manager class handling spawning logic. It defines enemy and projectile enums, EnemySpawnConfig, spawn configuration tables and implements all spawn flows for enemies, items, coins, effects, floaters, bosses, minibosses and related helpers and RPCs.
using Sandbox.Diagnostics;
using System;
public enum EnemyType
{
None, Barrel, BarrelExploding, BarrelMystery, Tree, Zombie, ZombieElite, Exploder, ExploderElite, Spitter, SpitterElite, Spiker, SpikerElite, Charger, ChargerElite, Runner, RunnerElite, Boss, Snaker,
MinibossAbsorber, MinibossCharger, MinibossCharger2, MinibossExploder, MinibossRunner, MinibossShielder, MinibossSlammer, MinibossSniper, MinibossSpiker, MinibossSpiker2, MinibossSpitter, MinibossTroll, MinibossZapper, MinibossZapper2, MinibossZoner,
Mushroom, Chest, ChestEvil, Turret, BossInvincibleGenerator, ExploderMini, Sword, ZombieTemporary,
}
public enum EnemyProjectileType { Normal, Fire, Freeze, Poison, Acid, Curse, }
public class EnemySpawnConfig
{
// Base weight curve (progress = _minutes / BOSS_SPAWN_MINUTES_DEFAULT)
/// <summary>Minimum progress (0-1) before this enemy can spawn.</summary>
public float ProgressThreshold;
/// <summary>Progress value where base weight reaches WeightMax.</summary>
public float ProgressEnd = 1f;
/// <summary>Base spawn weight at ProgressThreshold.</summary>
public float WeightMin;
/// <summary>Base spawn weight at ProgressEnd.</summary>
public float WeightMax;
/// <summary>Easing curve for base weight interpolation.</summary>
public EasingType BaseEasing = EasingType.Linear;
/// <summary>Per-difficulty WeightMin values [easy, normal, hard]. Overrides WeightMin if set.</summary>
public float[] WeightMinByDifficulty;
// Early spawn incentive (adds weight if few have spawned)
/// <summary>Minutes after which early spawn bonus starts applying.</summary>
public float EarlySpawnMinutes;
/// <summary>Minutes at which early spawn bonus reaches max value.</summary>
public float EarlySpawnMinutesEnd;
/// <summary>Max spawned count to still receive early spawn bonus.</summary>
public int EarlySpawnCountThreshold;
/// <summary>Bonus weight added at EarlySpawnMinutes.</summary>
public float EarlySpawnBonusMin;
/// <summary>Bonus weight added at EarlySpawnMinutesEnd.</summary>
public float EarlySpawnBonusMax;
/// <summary>Easing curve for early spawn bonus interpolation.</summary>
public EasingType EarlySpawnEasing = EasingType.Linear;
/// <summary>Minimum difficulty for early spawn bonus to apply (0 = all).</summary>
public int EarlySpawnMinDifficulty;
// Secondary catch-up bonus (adds weight scaled by how few have spawned)
/// <summary>Minutes after which catch-up bonus starts applying.</summary>
public float CatchupMinutes;
/// <summary>Minutes at which catch-up bonus reaches max value.</summary>
public float CatchupMinutesEnd;
/// <summary>Spawned count at which catch-up bonus becomes 0 (spawn factor maps from 0 to this).</summary>
public int CatchupCountThreshold;
/// <summary>Catch-up bonus at CatchupMinutes (usually 0).</summary>
public float CatchupBonusMin;
/// <summary>Maximum catch-up bonus (multiplied by spawn count factor).</summary>
public float CatchupBonusMax;
/// <summary>Minimum difficulty for catch-up bonus to apply (0 = all).</summary>
public int CatchupMinDifficulty;
// Late-game multiplier
/// <summary>Minutes after which late-game multiplier starts applying.</summary>
public float LateGameStartMinutes = 15f;
/// <summary>Minutes at which late-game multiplier reaches full value.</summary>
public float LateGameEndMinutes = 40f;
/// <summary>Weight multiplier at LateGameEndMinutes (1 = no change).</summary>
public float LateGameMultiplier = 1f;
// Population cap
/// <summary>Max existing count per difficulty [easy, normal, hard] before weight drops.</summary>
public int[] MaxCountByDifficulty;
/// <summary>Minimum weight ratio when at population cap (0 = no spawns at cap).</summary>
public float PopCapMinRatio = 0f;
/// <summary>Easing curve for population cap weight reduction.</summary>
public EasingType PopCapEasing = EasingType.Linear;
// Early threat boost (adds weight when total enemy threat is low)
/// <summary>Minutes after which threat boost starts applying.</summary>
public float ThreatBoostMinMinutes;
/// <summary>Minutes at which threat boost reaches max value.</summary>
public float ThreatBoostMaxMinutes;
/// <summary>Bonus weight added at ThreatBoostMinMinutes.</summary>
public float ThreatBoostWeightMin;
/// <summary>Bonus weight added at ThreatBoostMaxMinutes.</summary>
public float ThreatBoostWeightMax;
/// <summary>Minimum difficulty for threat boost to apply (0 = all).</summary>
public int ThreatBoostMinDifficulty;
/// <summary>Final weight multiplier per difficulty [easy, normal, hard].</summary>
public float[] DifficultyMult = new[] { 1f, 1f, 1f };
// Special flags
/// <summary>Minimum difficulty level required to spawn (0 = all difficulties).</summary>
public int MinDifficulty = 0;
}
public partial class Manager
{
private const float ITEM_SPAWN_VELOCITY_MIN = 40f;
private const float ITEM_SPAWN_VELOCITY_MAX = 80f;
[Sync] public float BossSpawnTime { get; private set; }
public float BossSpawnTimeOriginal => BOSS_SPAWN_MINUTES_DEFAULT * 60f;
public const float BOSS_SPAWN_MINUTES_DEFAULT = 13f;
[Sync] public bool HasSpawnedBoss { get; set; }
public Boss Boss { get; set; }
public Dictionary<EnemyType, int> EnemyExistingCounts = new();
public Dictionary<EnemyType, int> EnemySpawnedCounts = new();
private float _enemyThreatTotal;
private float _maxEarlyThreatLevel;
public int NumEnemies { get; set; }
public const float MAX_ENEMY_COUNT = 250;
//public const float MAX_BARREL_COUNT = 6;
Dictionary<EnemyType, float> _weightedValues = new();
// Post-boss miniboss spawning
private TimeSince _timeSinceMinibossCheck;
private bool _shouldSpawnMinibossNext;
// Starting Enemy Configuration
private const float STARTING_ENEMY_CHANCE_EXPERT = 0.5f; // 50% chance on Expert
private const float STARTING_ENEMY_CHANCE_CURSED = 0.95f; // 95% chance on Cursed
static Dictionary<EnemyType, float> StartingEnemyWeightsExpert = CreateStartingEnemyWeightsExpert();
static Dictionary<EnemyType, float> StartingEnemyWeightsCursed = CreateStartingEnemyWeightsCursed();
// Weights for Expert difficulty (Difficulty 1)
static Dictionary<EnemyType, float> CreateStartingEnemyWeightsExpert() => new()
{
{ EnemyType.ZombieElite, 15f },
{ EnemyType.Exploder, 8f },
{ EnemyType.ExploderElite, 3f },
{ EnemyType.Spitter, 8f },
{ EnemyType.SpitterElite, 3f },
{ EnemyType.Spiker, 6f },
{ EnemyType.SpikerElite, 2f },
{ EnemyType.Charger, 5f },
{ EnemyType.ChargerElite, 2f },
{ EnemyType.Runner, 7f },
{ EnemyType.RunnerElite, 2f },
{ EnemyType.ExploderMini, 6f },
{ EnemyType.Mushroom, 4f },
{ EnemyType.ChestEvil, 3f },
};
// Weights for Cursed difficulty (Difficulty 2)
static Dictionary<EnemyType, float> CreateStartingEnemyWeightsCursed() => new()
{
{ EnemyType.ZombieElite, 8f },
{ EnemyType.Exploder, 6f },
{ EnemyType.ExploderElite, 5f },
{ EnemyType.Spitter, 6f },
{ EnemyType.SpitterElite, 5f },
{ EnemyType.Spiker, 5f },
{ EnemyType.SpikerElite, 4f },
{ EnemyType.Charger, 4f },
{ EnemyType.ChargerElite, 4f },
{ EnemyType.Runner, 5f },
{ EnemyType.RunnerElite, 4f },
{ EnemyType.ExploderMini, 5f },
{ EnemyType.Mushroom, 5f },
{ EnemyType.ChestEvil, 5f },
};
[ConCmd( "reload_starting_enemy_weights" )]
public static void ReloadStartingEnemyWeights()
{
StartingEnemyWeightsExpert = CreateStartingEnemyWeightsExpert();
StartingEnemyWeightsCursed = CreateStartingEnemyWeightsCursed();
Log.Info( "Starting enemy weights reloaded" );
}
static Dictionary<EnemyType, EnemySpawnConfig> SpawnConfigs = CreateSpawnConfigs();
[ConCmd( "reload_spawn_configs" )]
public static void ReloadSpawnConfigs()
{
SpawnConfigs = CreateSpawnConfigs();
Log.Info( "Spawn configs reloaded" );
}
static Dictionary<EnemyType, EnemySpawnConfig> CreateSpawnConfigs() => new()
{
[EnemyType.Zombie] = new()
{
ProgressThreshold = 0f, ProgressEnd = 1f,
WeightMin = 100f, WeightMax = 100f,
LateGameStartMinutes = 1f, LateGameEndMinutes = 30f, LateGameMultiplier = 0.01f,
DifficultyMult = new[] { 1.05f, 1f, 0.75f },
},
[EnemyType.ZombieElite] = new()
{
ProgressThreshold = 0.4f, ProgressEnd = 1.4f,
WeightMinByDifficulty = new[] { 0.1f, 0.15f, 0.175f }, WeightMax = 42f,
BaseEasing = EasingType.QuadIn,
EarlySpawnMinutes = 0.1f, EarlySpawnMinutesEnd = 2f,
EarlySpawnCountThreshold = 4, EarlySpawnBonusMin = 0.075f, EarlySpawnBonusMax = 0.25f,
ThreatBoostMinMinutes = 0.1f, ThreatBoostMaxMinutes = 1.5f, ThreatBoostWeightMin = 0.05f, ThreatBoostWeightMax = 2f,
DifficultyMult = new[] { 0.9f, 1.1f, 2f },
},
[EnemyType.Exploder] = new()
{
ProgressThreshold = 0.04f, ProgressEnd = 0.75f,
WeightMin = 2.8f, WeightMax = 6.8f,
EarlySpawnMinutes = 0.1f, EarlySpawnMinutesEnd = 0.1f,
EarlySpawnCountThreshold = 0, EarlySpawnBonusMin = 0.05f, EarlySpawnBonusMax = 0.05f,
LateGameStartMinutes = 18f, LateGameEndMinutes = 50f, LateGameMultiplier = 0.01f,
CatchupMinutes = 1f, CatchupMinutesEnd = 3f, CatchupCountThreshold = 3, CatchupBonusMax = 2f,
MaxCountByDifficulty = new[] { 12, 12, 12 }, PopCapMinRatio = 0.1f,
},
[EnemyType.ExploderElite] = new()
{
ProgressThreshold = 0.4f, ProgressEnd = 1.4f,
WeightMin = 0.025f, WeightMax = 0.8f,
BaseEasing = EasingType.SineIn,
EarlySpawnMinutes = 1.5f, EarlySpawnMinutesEnd = 4f,
EarlySpawnCountThreshold = 1, EarlySpawnBonusMin = 0.05f, EarlySpawnBonusMax = 0.25f,
EarlySpawnMinDifficulty = 1,
LateGameStartMinutes = 18f, LateGameEndMinutes = 40f, LateGameMultiplier = 7f,
MaxCountByDifficulty = new[] { 2, 6, 8 }, PopCapMinRatio = 0f,
ThreatBoostMinMinutes = 0.5f, ThreatBoostMaxMinutes = 3.5f, ThreatBoostWeightMin = 0.05f, ThreatBoostWeightMax = 0.5f,
DifficultyMult = new[] { 0.7f, 1.1f, 1.3f },
},
[EnemyType.Spitter] = new()
{
ProgressThreshold = 0.045f, ProgressEnd = 1f,
WeightMin = 0.08f, WeightMax = 4.3f,
EarlySpawnMinutes = 0.1f, EarlySpawnMinutesEnd = 2f,
EarlySpawnCountThreshold = 2, EarlySpawnBonusMin = 0.05f, EarlySpawnBonusMax = 0.8f,
EarlySpawnEasing = EasingType.SineIn,
CatchupMinutes = 2f, CatchupMinutesEnd = 7f, CatchupCountThreshold = 4, CatchupBonusMax = 0.8f,
LateGameStartMinutes = 15.5f, LateGameEndMinutes = 50f, LateGameMultiplier = 12f,
MaxCountByDifficulty = new[] { 6, 11, 13 }, PopCapMinRatio = 0.075f,
ThreatBoostMinMinutes = 0.1f, ThreatBoostMaxMinutes = 2f, ThreatBoostWeightMin = 0.05f, ThreatBoostWeightMax = 2f,
DifficultyMult = new[] { 0.9f, 1f, 1.2f },
},
[EnemyType.SpitterElite] = new()
{
ProgressThreshold = 0.25f, ProgressEnd = 1f,
WeightMin = 0.1f, WeightMax = 1.3f,
BaseEasing = EasingType.SineIn,
EarlySpawnMinutes = 1.3f, EarlySpawnMinutesEnd = 5f,
EarlySpawnCountThreshold = 3, EarlySpawnBonusMin = 0.025f, EarlySpawnBonusMax = 0.05f,
EarlySpawnMinDifficulty = 1,
CatchupMinutes = 4f, CatchupMinutesEnd = 10f, CatchupCountThreshold = 5, CatchupBonusMax = 2f,
CatchupMinDifficulty = 1,
LateGameStartMinutes = 20f, LateGameEndMinutes = 90f, LateGameMultiplier = 40f,
MaxCountByDifficulty = new[] { 3, 7, 10 }, PopCapMinRatio = 0.0075f,
ThreatBoostMinMinutes = 0.3f, ThreatBoostMaxMinutes = 3f, ThreatBoostWeightMin = 0.05f, ThreatBoostWeightMax = 1f,
ThreatBoostMinDifficulty = 1,
DifficultyMult = new[] { 0.52f, 1.05f, 1.25f },
},
[EnemyType.Spiker] = new()
{
ProgressThreshold = 0.3f, ProgressEnd = 1f,
WeightMin = 0.08f, WeightMax = 3.8f,
BaseEasing = EasingType.SineIn,
EarlySpawnMinutes = 1f, EarlySpawnMinutesEnd = 6f,
EarlySpawnCountThreshold = 2, EarlySpawnBonusMin = 0f, EarlySpawnBonusMax = 0.06f,
CatchupMinutes = 4f, CatchupMinutesEnd = 7f, CatchupCountThreshold = 4, CatchupBonusMax = 0.5f,
LateGameStartMinutes = 16f, LateGameEndMinutes = 40f, LateGameMultiplier = 5f,
MaxCountByDifficulty = new[] { 5, 12, 16 }, PopCapMinRatio = 0.1f,
ThreatBoostMinMinutes = 0.5f, ThreatBoostMaxMinutes = 2.5f, ThreatBoostWeightMin = 0.05f, ThreatBoostWeightMax = 1f,
DifficultyMult = new[] { 0.8f, 1.1f, 1.3f },
},
[EnemyType.SpikerElite] = new()
{
ProgressThreshold = 0.5f, ProgressEnd = 1f,
WeightMin = 0.027f, WeightMax = 1.7f,
BaseEasing = EasingType.SineIn,
EarlySpawnMinutes = 5f, EarlySpawnMinutesEnd = 8f,
EarlySpawnCountThreshold = 1, EarlySpawnBonusMin = 0.02f, EarlySpawnBonusMax = 0.04f,
LateGameStartMinutes = 16.5f, LateGameEndMinutes = 40f, LateGameMultiplier = 15f,
MaxCountByDifficulty = new[] { 3, 6, 10 }, PopCapMinRatio = 0f,
ThreatBoostMinMinutes = 0.5f, ThreatBoostMaxMinutes = 3.5f, ThreatBoostWeightMin = 0f, ThreatBoostWeightMax = 0.5f,
DifficultyMult = new[] { 0.65f, 1.1f, 1.3f },
},
[EnemyType.Charger] = new()
{
ProgressThreshold = 0.45f, ProgressEnd = 1f,
WeightMin = 0.055f, WeightMax = 1.9f,
BaseEasing = EasingType.SineIn,
CatchupMinutes = 4f, CatchupMinutesEnd = 7f, CatchupCountThreshold = 2, CatchupBonusMax = 0.25f,
LateGameStartMinutes = 16f, LateGameEndMinutes = 40f, LateGameMultiplier = 15f,
MaxCountByDifficulty = new[] { 3, 5, 7 }, PopCapMinRatio = 0f,
ThreatBoostMinMinutes = 0.3f, ThreatBoostMaxMinutes = 2.5f, ThreatBoostWeightMin = 0f, ThreatBoostWeightMax = 1f,
DifficultyMult = new[] { 0.9f, 1.05f, 1.3f },
},
[EnemyType.ChargerElite] = new()
{
ProgressThreshold = 0.66f, ProgressEnd = 1f,
WeightMin = 0.045f, WeightMax = 0.95f,
BaseEasing = EasingType.SineIn,
EarlySpawnMinutes = 7f, EarlySpawnMinutesEnd = 11f,
EarlySpawnCountThreshold = 0, EarlySpawnBonusMin = 0f, EarlySpawnBonusMax = 0.025f,
LateGameStartMinutes = 17f, LateGameEndMinutes = 60f, LateGameMultiplier = 25f,
MaxCountByDifficulty = new[] { 2, 4, 6 }, PopCapMinRatio = 0f,
ThreatBoostMinMinutes = 0.3f, ThreatBoostMaxMinutes = 4.5f, ThreatBoostWeightMin = 0f, ThreatBoostWeightMax = 0.35f,
DifficultyMult = new[] { 0.5f, 1.05f, 1.3f },
},
[EnemyType.Runner] = new()
{
ProgressThreshold = 0.4f, ProgressEnd = 1f,
WeightMin = 0.055f, WeightMax = 4.5f,
BaseEasing = EasingType.SineIn,
CatchupMinutes = 4f, CatchupMinutesEnd = 8f, CatchupCountThreshold = 4,
CatchupBonusMin = 0.01f, CatchupBonusMax = 1f, CatchupMinDifficulty = 1,
LateGameStartMinutes = 16f, LateGameEndMinutes = 40f, LateGameMultiplier = 5f,
MaxCountByDifficulty = new[] { 13, 30, 40 }, PopCapMinRatio = 0f,
PopCapEasing = EasingType.QuadIn,
ThreatBoostMinMinutes = 0.2f, ThreatBoostMaxMinutes = 3f, ThreatBoostWeightMin = 0f, ThreatBoostWeightMax = 1f,
DifficultyMult = new[] { 0.85f, 1.1f, 1.3f },
},
[EnemyType.RunnerElite] = new()
{
ProgressThreshold = 0.75f, ProgressEnd = 1f,
WeightMin = 0.04f, WeightMax = 2.1f,
BaseEasing = EasingType.SineIn,
EarlySpawnMinutes = 8f, EarlySpawnMinutesEnd = 8f,
EarlySpawnCountThreshold = 0, EarlySpawnBonusMin = 0.01f, EarlySpawnBonusMax = 0.01f,
CatchupMinutes = 7f, CatchupMinutesEnd = 12f, CatchupCountThreshold = 1, CatchupBonusMax = 0.25f,
LateGameStartMinutes = 17.5f, LateGameEndMinutes = 60f, LateGameMultiplier = 15f,
MaxCountByDifficulty = new[] { 3, 6, 8 }, PopCapMinRatio = 0f,
ThreatBoostMinMinutes = 0.5f, ThreatBoostMaxMinutes = 4f, ThreatBoostWeightMin = 0f, ThreatBoostWeightMax = 0.25f,
DifficultyMult = new[] { 0.6f, 1f, 1.25f },
},
[EnemyType.ExploderMini] = new()
{
ProgressThreshold = 0.045f, ProgressEnd = 1f,
WeightMin = 0.055f, WeightMax = 2.1f,
EarlySpawnMinutes = 0.1f, EarlySpawnMinutesEnd = 2f,
EarlySpawnCountThreshold = 2, EarlySpawnBonusMin = 0.05f, EarlySpawnBonusMax = 0.3f,
EarlySpawnEasing = EasingType.SineIn,
EarlySpawnMinDifficulty = 1,
CatchupMinutes = 2f, CatchupMinutesEnd = 7f, CatchupCountThreshold = 4, CatchupBonusMax = 0.6f, CatchupMinDifficulty = 1,
LateGameStartMinutes = 16.5f, LateGameEndMinutes = 60f, LateGameMultiplier = 13f,
MaxCountByDifficulty = new[] { 10, 11, 12 }, PopCapMinRatio = 0.1f,
ThreatBoostMinMinutes = 0.3f, ThreatBoostMaxMinutes = 2.5f, ThreatBoostWeightMin = 0f, ThreatBoostWeightMax = 1f, ThreatBoostMinDifficulty = 1,
DifficultyMult = new[] { 0.5f, 1.05f, 1.3f },
},
};
public bool ShowPreciseNumbers { get; set; }
private float _minutes;
void SpawnStartingThings()
{
EnemyExistingCounts.Clear();
EnemySpawnedCounts.Clear();
_enemyThreatTotal = 0f;
switch( Difficulty )
{
case 0: default: _maxEarlyThreatLevel = 1.5f; break;
case 1: _maxEarlyThreatLevel = 5f; break;
case 2: _maxEarlyThreatLevel = 15f; break;
}
_minutes = 0f;
foreach ( EnemyType value in Enum.GetValues( typeof( EnemyType ) ) )
{
if ( (int)value == 0 ) continue;
EnemyExistingCounts[value] = 0;
EnemySpawnedCounts[value] = 0;
}
for ( int i = 0; i < 3; i++ )
SpawnEnemyAtRandomPos( EnemyType.Barrel, spawnInstantly: true );
for ( int i = 0; i < 2; i++ )
SpawnEnemyAtRandomPos( EnemyType.BarrelExploding, spawnInstantly: true );
for ( int i = 0; i < 10; i++ )
SpawnEnemyAtRandomPos( EnemyType.Tree, spawnInstantly: true );
// Spawn starting enemy for Expert and Cursed difficulties
SpawnStartingEnemy();
for ( int i = 0; i < 5; i++ )
{
//float dist = 720f;
//SpawnEnemy( "zombie", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "zombie_elite", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
// SpawnEnemy( "barrel", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ), spawnInstantly: true );
// SpawnEnemy( "tree", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ), spawnInstantly: true );
}
for ( int i = 0; i < 2; i++ )
{
//float dist = 720f;
//SpawnEnemy( "runner", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "runner_elite", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
}
for ( int i = 0; i < 1; i++ )
{
//float dist = 720f;
//SpawnEnemy( "exploder", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "exploder_elite", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "spitter", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "spitter_elite", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "spiker", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "spiker_elite", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
}
for ( int i = 0; i < 1; i++ )
{
//float dist = 720f;
//SpawnEnemy( "charger", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
//SpawnEnemy( "charger_elite", new Vector2( Game.Random.Float( -1f, 1f ) * dist, Game.Random.Float( -1f, 1f ) * dist ) );
}
//SpawnEnemy( "miniboss_spitter", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_troll", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_runner", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_charger", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_slammer", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_shielder", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_spiker", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_exploder", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_zapper", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnEnemy( "miniboss_zapper_2", new Vector2( Game.Random.Float( -1f, 1f ) * 33, Game.Random.Float( -1f, 1f ) * 33 ) );
//SpawnBoss();
//SpawnSnaker( pos: new Vector2( Game.Random.Float( -300f, 300f ), Game.Random.Float( -300f, 300f ) ), numSegments: 12 );
}
/// <summary>
/// Attempts to spawn a starting enemy for Expert and Cursed difficulties.
/// Returns true if an enemy was spawned.
/// </summary>
private bool SpawnStartingEnemy()
{
if ( DontSpawnRandomEnemies )
return false;
// Only spawn on Expert (1) or Cursed (2) difficulty
if ( Difficulty < 1 )
return false;
// Get spawn chance based on difficulty
float spawnChance = Difficulty == 1 ? STARTING_ENEMY_CHANCE_EXPERT : STARTING_ENEMY_CHANCE_CURSED;
// Roll for spawn chance
if ( Game.Random.Float( 0f, 1f ) > spawnChance )
return false;
// Get weights based on difficulty
var weights = Difficulty == 1 ? StartingEnemyWeightsExpert : StartingEnemyWeightsCursed;
// Select random enemy type using weighted random
var enemyType = Utils.GetWeightedRandom( weights );
if ( enemyType == EnemyType.None )
return false;
// Spawn the enemy at a random position
SpawnEnemyAtRandomPos( enemyType, spawnInstantly: false );
return true;
}
Player SpawnPlayer( Connection channel, int lobbySlot = -1 )
{
Assert.True( Networking.IsHost, $"Client tried to SpawnPlayer: {channel.DisplayName}" );
var pos = new Vector3( Game.Random.Float( -30f, 30f ), Game.Random.Float( -30f, 30f ), 0f );
var playerGo = GameObject.Clone( "prefabs/player.prefab", new CloneConfig { Name = channel.DisplayName, StartEnabled = true, Transform = new Transform( pos ) } );
var player = playerGo.Components.Get<Player>( true );
playerGo.NetworkSpawn( channel );
if(lobbySlot != -1)
{
player.LobbySlotIndex = lobbySlot;
}
return player;
}
void HandleEnemySpawn()
{
Assert.True( Networking.IsHost );
var t = ElapsedTime;
var minutes = t / 60f;
var spawnTime = Utils.Map( NumEnemies, 0, MAX_ENEMY_COUNT, 0.025f, 0.33f, EasingType.QuadOut )
* Utils.Map( minutes, 0f, 1.25f, 1.6f, 1f )
* Utils.Map( minutes, 0f, 4f, 3.2f, 1f )
* Utils.Map( minutes, 10f, 15f, 1f, 0.75f )
* Utils.Map( minutes, 15f, 40f, 1f, 0.5f );
spawnTime *= Utils.Map( AlivePlayers.Count, 1, 3, 1f, 0.6f );
if ( HasSpawnedBoss )
{
// slow spawning right after boss spawn, then speed back up
var lerpAmount = Utils.Map( t, BossSpawnTime, BossSpawnTime + Utils.Select( Difficulty, 2f, 1.5f, 1.25f ), 1f, 0f, EasingType.SineIn );
var slowSpawnFactor = Utils.Select( Difficulty, 7f, 3.5f, 2.2f );
spawnTime *= MathX.Lerp( 1f, slowSpawnFactor, lerpAmount );
//Log.Info( $"spawnTime after boss adjustment: {spawnTime} slowSpawnFactor: {slowSpawnFactor} lerpAmount: {lerpAmount}" );
}
if ( _timeSinceSpawnEnemy > spawnTime )
{
SpawnRandomEnemy();
_timeSinceSpawnEnemy = 0f;
}
HandleBossSpawn();
}
void HandleBossSpawn()
{
if ( HasSpawnedBoss )
return;
BossSpawnTime = BossSpawnTimeOriginal;
// todo: only calculate this when a player changes boss spawn time
foreach ( Player player in Players )
BossSpawnTime -= player.GetSyncStat( PlayerStat.BossSpawnSecondsEarly );
if ( ElapsedTime > BossSpawnTime )
SpawnBoss();
}
void UpdateMinibossSpawnFlag()
{
if ( !HasSpawnedBoss )
return;
// Time since boss (in minutes) determines how frequent miniboss replacements are
float minutesSinceBoss = (ElapsedTime - BossSpawnTime) / 60f;
// Interval decreases over time
var lerpTimeMinutes = Utils.Select( Difficulty, 55f, 45f, 40f );
var startIntervalSeconds = Utils.Select( Difficulty, 150f, 80f, 55f );
float interval = Utils.Map( minutesSinceBoss, 0f, lerpTimeMinutes, startIntervalSeconds, 0f, EasingType.Linear );
if ( _timeSinceMinibossCheck > interval )
{
_shouldSpawnMinibossNext = true;
_timeSinceMinibossCheck = 0f;
}
if ( ShowDebug )
{
var str = $"_timeSinceMinibossCheck: {_timeSinceMinibossCheck}\nlerpTimeMinutes: {lerpTimeMinutes}\nstartIntervalSeconds: {startIntervalSeconds}\nminutesSinceBoss: {minutesSinceBoss}interval: {interval}_shouldSpawnMinibossNext: {_shouldSpawnMinibossNext}";
Gizmo.Draw.Color = Color.White.WithAlpha( 0.5f );
Gizmo.Draw.ScreenText( str, new Vector2( 140, 90 ) );
}
}
void SpawnRandomEnemy()
{
if ( DontSpawnRandomEnemies )
return;
if ( NumEnemies >= MAX_ENEMY_COUNT )
return;
// Check for miniboss replacement
UpdateMinibossSpawnFlag();
if ( _shouldSpawnMinibossNext )
{
_shouldSpawnMinibossNext = false;
SpawnMiniboss( GetRandomSpawnPos() );
return;
}
_weightedValues.Clear();
var t = ElapsedTime;
t *= GetTimeFactor( t );
switch ( Difficulty )
{
case 0:
default:
if ( HasSpawnedBoss && t > BossSpawnTime )
t = BossSpawnTime + (t - BossSpawnTime) * 0.45f;
else
t *= 0.65f;
break;
case 1:
break;
}
_minutes = t / 60f;
AddEnemySpawnWeight( EnemyType.Barrel );
AddEnemySpawnWeight( EnemyType.BarrelExploding );
AddEnemySpawnWeight( EnemyType.Tree );
AddEnemySpawnWeight( EnemyType.Zombie );
AddEnemySpawnWeight( EnemyType.ZombieElite );
AddEnemySpawnWeight( EnemyType.Exploder );
AddEnemySpawnWeight( EnemyType.ExploderElite );
AddEnemySpawnWeight( EnemyType.Spitter );
AddEnemySpawnWeight( EnemyType.SpitterElite );
AddEnemySpawnWeight( EnemyType.Spiker );
AddEnemySpawnWeight( EnemyType.SpikerElite );
AddEnemySpawnWeight( EnemyType.Charger );
AddEnemySpawnWeight( EnemyType.ChargerElite );
AddEnemySpawnWeight( EnemyType.Runner );
AddEnemySpawnWeight( EnemyType.RunnerElite );
if ( Difficulty >= 1 || _minutes >= 17f )
AddEnemySpawnWeight( EnemyType.ExploderMini );
var total = 0f;
foreach ( var pair in _weightedValues )
total += pair.Value;
var rand = Game.Random.Float( 0f, total );
//Log.Info( $"-------------------- total: {total} rand: {rand}" );
var currentTotal = 0f;
foreach ( var pair in _weightedValues )
{
var enemyType = pair.Key;
currentTotal += pair.Value;
//Log.Info( $"{enemyType} currentTotal: {currentTotal}" );
if ( rand < currentTotal )
{
//Log.Info( $"spawning {enemyType}!" );
SpawnEnemyNearPlayerOrRandom( enemyType );
break;
}
}
// todo: randomize chances over time for each enemy, depending on the run? (to encourage picking counter perks)
// or at least favor some types
}
float GetTimeFactor( float t )
{
var timeFactor = 1f;
var numEnemyKills = 0;
foreach ( var player in Players )
numEnemyKills += (int)player.GetSyncStat( PlayerStat.NumEnemiesKilled );
var minutes = t / 60f;
var expectedKills = Utils.Map( minutes, 0f, BOSS_SPAWN_MINUTES_DEFAULT, 0f, 1000 ) * Utils.Map( Players.Count, 1, MAX_PLAYERS, 1f, 2f );
if ( numEnemyKills > expectedKills )
timeFactor *= numEnemyKills / expectedKills;
return timeFactor;
}
void AddEnemySpawnWeight( EnemyType enemyType )
{
var chance = GetEnemySpawnWeight( enemyType );
if ( chance > 0f )
_weightedValues.Add( enemyType, chance );
}
float GetEnemySpawnWeight( EnemyType enemyType )
{
// Special cases with unique logic
if ( enemyType == EnemyType.Barrel )
return GetBarrelSpawnWeight( _minutes, exploding: false );
if ( enemyType == EnemyType.BarrelExploding )
return GetBarrelSpawnWeight( _minutes, exploding: true );
if ( enemyType == EnemyType.Tree )
return Utils.Map( EnemyExistingCounts[enemyType], 0, 10, 0.03f, 0f, EasingType.QuadOut );
if ( !SpawnConfigs.TryGetValue( enemyType, out var cfg ) )
return 0f;
if ( Difficulty < cfg.MinDifficulty )
return 0f;
return CalculateSpawnWeight( cfg, enemyType );
}
float CalculateSpawnWeight( EnemySpawnConfig cfg, EnemyType enemyType )
{
var progress = _minutes / BOSS_SPAWN_MINUTES_DEFAULT;
// Base weight from progress (0 if below threshold, but continue to apply bonuses)
float weight;
if ( progress < cfg.ProgressThreshold )
{
weight = 0f;
}
else
{
var weightMin = cfg.WeightMinByDifficulty != null
? cfg.WeightMinByDifficulty[Difficulty]
: cfg.WeightMin;
weight = Utils.Map( progress, cfg.ProgressThreshold, cfg.ProgressEnd,
weightMin, cfg.WeightMax, cfg.BaseEasing );
}
// ZombieElite mid-progress boost
if ( enemyType == EnemyType.ZombieElite && progress > 0.8f )
weight *= Utils.Map( progress, 0.8f, 1f, 1f, 1.1f, EasingType.Linear );
// Early spawn incentive
if ( cfg.EarlySpawnBonusMax > 0 && Difficulty >= cfg.EarlySpawnMinDifficulty
&& _minutes > cfg.EarlySpawnMinutes && EnemySpawnedCounts[enemyType] <= cfg.EarlySpawnCountThreshold )
{
weight += Utils.Map( _minutes, cfg.EarlySpawnMinutes, cfg.EarlySpawnMinutesEnd,
cfg.EarlySpawnBonusMin, cfg.EarlySpawnBonusMax, cfg.EarlySpawnEasing );
}
// Charger has difficulty-dependent early spawn (special case)
if ( enemyType == EnemyType.Charger )
{
var earlyMinutes = Utils.Select( Difficulty, 5f, 3f, 1.5f );
var earlyCount = Utils.Select( Difficulty, 0, 2, 4 );
if ( _minutes > earlyMinutes && EnemySpawnedCounts[enemyType] <= earlyCount )
weight += Utils.Select( Difficulty, 0.02f, 0.25f, 0.5f );
}
// Secondary catch-up bonus (spawn factor naturally goes to 0 at threshold)
if ( cfg.CatchupBonusMax > 0 && Difficulty >= cfg.CatchupMinDifficulty && _minutes > cfg.CatchupMinutes )
{
var catchupBonus = Utils.Map( _minutes, cfg.CatchupMinutes, cfg.CatchupMinutesEnd, cfg.CatchupBonusMin, cfg.CatchupBonusMax );
var spawnFactor = Utils.Map( EnemySpawnedCounts[enemyType], 0, cfg.CatchupCountThreshold, 1f, 0f );
weight += catchupBonus * MathF.Max( 0f, spawnFactor );
}
// Late-game multiplier
if ( cfg.LateGameMultiplier != 1f && _minutes > cfg.LateGameStartMinutes )
{
weight *= Utils.Map( _minutes, cfg.LateGameStartMinutes, cfg.LateGameEndMinutes,
1f, cfg.LateGameMultiplier, EasingType.Linear );
}
// Population cap
if ( cfg.MaxCountByDifficulty != null )
{
var maxCount = cfg.MaxCountByDifficulty[Difficulty];
weight *= Utils.Map( EnemyExistingCounts[enemyType], 0, maxCount,
1f, cfg.PopCapMinRatio, cfg.PopCapEasing );
}
// Early threat boost
if ( cfg.ThreatBoostWeightMax > 0 && Difficulty >= cfg.ThreatBoostMinDifficulty
&& _minutes > cfg.ThreatBoostMinMinutes && _enemyThreatTotal < _maxEarlyThreatLevel )
{
weight += Utils.Map( _minutes, cfg.ThreatBoostMinMinutes, cfg.ThreatBoostMaxMinutes,
cfg.ThreatBoostWeightMin, cfg.ThreatBoostWeightMax );
}
// Difficulty multiplier
weight *= cfg.DifficultyMult[Difficulty];
return weight;
}
float GetBarrelSpawnWeight( float minutes, bool exploding )
{
var numBarrels = EnemyExistingCounts[EnemyType.Barrel] + EnemyExistingCounts[EnemyType.BarrelExploding];
//if ( numBarrels >= MAX_BARREL_COUNT )
// return 0f;
float barrelWeight = minutes < 0.3f ? 0f : Utils.Map( minutes, 0.3f, 10f, 0.45f, 8f );
float additionalBarrelChance = 0f;
foreach ( var player in AlivePlayers )
additionalBarrelChance += player.GetSyncStat( PlayerStat.BarrelChanceAdditional );
barrelWeight *= (1f + additionalBarrelChance);
if ( exploding )
barrelWeight *= 0.3f;
barrelWeight *= Utils.Map( numBarrels, 0, 12, 1f, 0.05f, EasingType.SineIn );
return barrelWeight;
}
public Vector2 GetRandomSpawnPos( float buffer = 0f )
{
return new Vector2( Game.Random.Float( BOUNDS_MIN_SPAWN.x + buffer, BOUNDS_MAX_SPAWN.x - buffer ), Game.Random.Float( BOUNDS_MIN_SPAWN.y + buffer, BOUNDS_MAX_SPAWN.y - buffer ) );
}
public void SpawnEnemyAtRandomPos( EnemyType enemyType, bool spawnInstantly = false )
{
SpawnEnemy( enemyType, GetRandomSpawnPos(), spawnInstantly );
}
void SpawnEnemyNearPlayerOrRandom( EnemyType enemyType )
{
var cursedPlayers = AlivePlayers.Where( p => p.GetSyncStat( PlayerStat.EnemySpawnNearPlayer ) > 0f ).ToList();
var spawnNearbyChance = 0.5f;
if ( cursedPlayers.Count > 0 && Game.Random.Float( 0f, 1f ) < spawnNearbyChance )
{
var target = Game.Random.FromList( cursedPlayers );
var dir = Utils.GetRandomVector();
var dist = Game.Random.Float( 0f, 100f );
var pos = target.Position2D + dir * dist + target.Velocity * Game.Random.Float( 0f, 3f );
SpawnEnemy( enemyType, pos );
}
else
{
SpawnEnemyAtRandomPos( enemyType );
}
}
[Rpc.Host( NetFlags.Reliable )]
public void SpawnEnemyRpc( EnemyType enemyType, Vector2 pos, bool spawnInstantly = false, float rotAngle = 0f, bool playSfx = true, Player playerCreator = null )
{
SpawnEnemy( enemyType, pos, spawnInstantly, rotAngle, playSfx, playerCreator );
}
public void SpawnEnemy( EnemyType enemyType, Vector2 pos, bool spawnInstantly = false, float rotAngle = 0f, bool playSfx = true, Player playerCreator = null )
{
if ( !IsProxy )
{
EnemyExistingCounts[enemyType]++;
EnemySpawnedCounts[enemyType]++;
_enemyThreatTotal += GetThreatLevelForEnemyType( enemyType );
}
SpawnEnemy( GetStringForEnemyType( enemyType ), pos, spawnInstantly, rotAngle, playSfx, playerCreator );
}
[Rpc.Broadcast]
public void SpawnEnemy( string name, Vector2 pos, bool spawnInstantly = false, float rotAngle = 0f, bool playSfx = true, Player playerCreator = null )
{
if ( name != "boss" ) // boss spawns out of bounds and then moves in, so don't clamp its spawn position
pos = ClampPosToBounds( pos );
//if ( IsProxy )
if ( !Networking.IsHost )
{
if ( name == "chest" )
Log.Warning( $"[ChestSpawn] Non-host received chest SpawnEnemy RPC — skipping (IsProxy={IsProxy})" );
return;
}
if( name == "boss" )
{
if(HasSpawnedBoss)
{
Log.Warning("Spawning a boss when one is already active!");
}
}
if( name == "tree" )
{
if( Manager.Instance.Difficulty == 1 )
name = "tree_red";
else if ( Manager.Instance.Difficulty == 2 )
name = "tree_snow";
}
else if( name == "spitter" )
{
var t = ElapsedTime;
var minutes = t / 60f;
var projectileType = GetRandomEnemyProjectileType( minutes );
name = GetEnemyNameForProjectileType( projectileType, elite: false );
}
else if ( name == "spitter_elite" )
{
var t = ElapsedTime;
var minutes = t / 60f;
var projectileType = GetRandomEnemyProjectileType( minutes );
name = GetEnemyNameForProjectileType( projectileType, elite: true );
}
var zPos = spawnInstantly ? 0f : -150f;
var yaw = MathF.Abs( rotAngle ) > 0f ? rotAngle : Game.Random.Float( 0f, 360f );
var enemyGo = GameObject.Clone( $"prefabs/enemies/{name}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( position: new Vector3( pos.x, pos.y, zPos ), rotation: new Angles( 0f, yaw, 0f ) ) } );
if ( enemyGo == null )
{
Log.Error( $"Failed to spawn enemy '{name}' - prefab not found!" );
return;
}
//enemyGo.Transform.ClearInterpolation();
var enemy = enemyGo.GetComponent<Enemy>();
enemy.Health = enemy.MaxHealth;
enemy.ShouldSpawnInstantly = spawnInstantly;
if ( playerCreator.IsValid() )
enemy.PlayerCreator = playerCreator;
if ( !spawnInstantly && enemy.ShouldCreateSpawnClouds )
{
var cloudPos = new Vector3( pos.x + Game.Random.Float( -5f, 5f ), pos.y + Game.Random.Float( -5f, 5f ), 15f );
GameObject.Clone( "prefabs/effects/enemy_spawn_clouds.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( cloudPos ) } );
if ( playSfx )
PlaySfxNearby( "zombie.dirt", pos, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.4f, maxDist: 350f );
}
enemyGo.NetworkSpawn();
if ( name == "chest" )
Log.Info( $"[ChestSpawn] Chest NetworkSpawned successfully at {pos}" );
NumEnemies++;
}
public EnemyProjectileType GetRandomEnemyProjectileType( float minutes )
{
float startAmount;
float endAmount;
switch ( Difficulty )
{
case 0:
default:
startAmount = 0f;
endAmount = 0.25f;
break;
case 1:
startAmount = 0.1f;
endAmount = 1f;
break;
case 2:
startAmount = 0.25f;
endAmount = 1.1f;
break;
}
var specialWeight = Utils.Map( minutes, 0f, BOSS_SPAWN_MINUTES_DEFAULT, startAmount, endAmount, EasingType.Linear );
if ( minutes > BOSS_SPAWN_MINUTES_DEFAULT )
specialWeight *= Utils.Map( minutes, BOSS_SPAWN_MINUTES_DEFAULT, 60f, 1f, 10f, EasingType.Linear );
var weights = new Dictionary<EnemyProjectileType, float>
{
{ EnemyProjectileType.Normal, 1f },
{ EnemyProjectileType.Acid, specialWeight * Utils.Select( Difficulty, 0f, 0.6f, 0.75f ) },
{ EnemyProjectileType.Fire, specialWeight },
{ EnemyProjectileType.Freeze, specialWeight },
{ EnemyProjectileType.Poison, specialWeight },
};
var canSpawnCurse = (Difficulty == 0 && minutes > 20f) || (Difficulty == 1 && minutes > 15f) || (Difficulty >= 2 && minutes > 10f);
if ( canSpawnCurse )
{
var curseWeight = specialWeight * Utils.Select( Difficulty, 0.25f, 0.5f, 0.8f )
* Utils.Map( minutes, Utils.Select( Difficulty, 22f, 19f, 18f ), 60f, 1f, 10f )
* Utils.Map( minutes, 60f, 120f, 1f, 30f );
weights.Add( EnemyProjectileType.Curse, curseWeight );
}
var validWeights = weights.Where( kv => kv.Value > 0f ).ToList();
if ( validWeights.Count == 0 )
return EnemyProjectileType.Normal;
float totalWeight = validWeights.Sum( kv => kv.Value );
float rand = Game.Random.Float( 0f, totalWeight );
float cumulative = 0f;
foreach ( var kv in validWeights )
{
cumulative += kv.Value;
if ( rand < cumulative )
return kv.Key;
}
return validWeights.Last().Key;
}
public static string GetEnemyNameForProjectileType( EnemyProjectileType projectileType, bool elite )
{
var name = elite ? "spitter_elite" : "spitter";
switch ( projectileType )
{
case EnemyProjectileType.Normal: default: break;
case EnemyProjectileType.Fire: name += "_fire"; break;
case EnemyProjectileType.Freeze: name += "_freeze"; break;
case EnemyProjectileType.Poison: name += "_poison"; break;
case EnemyProjectileType.Acid: name += "_acid"; break;
case EnemyProjectileType.Curse: name += "_curse"; break;
}
return name;
}
public void RemoveEnemy( EnemyType enemyType )
{
EnemyExistingCounts[enemyType]--;
NumEnemies--;
if ( !HasSpawnedBoss )
{
var minutes = ElapsedTime / 60f;
if ( Game.Random.Float( 0f, 1f ) < Utils.Map( NumEnemies, 0, MAX_ENEMY_COUNT, 1f, 0f, EasingType.SineIn ) * Utils.Map( minutes, 0f, 10f, 0f, 1f ) )
SpawnRandomEnemy();
}
}
public void SpawnBoss()
{
var spawnPos = new Vector2( Game.Random.Float( -400f, 400f ), 2500f );
SpawnEnemy( EnemyType.Boss, spawnPos );
PlaySfxNearbyRpcImportant( "boss.fanfare", Vector2.Zero, pitch: 1f, volume: 1.4f, maxDist: 2000f );
ShakeCamRpc( strength: 2.5f, 1f, EasingType.Linear, useRealTime: true );
HasSpawnedBoss = true;
_timeSinceMinibossCheck = 0f;
}
public void SpawnMiniboss( Vector2 pos, bool spawnInstantly = false )
{
EnemyType enemyType;
int rand = Game.Random.Int( 0, 14 );
switch ( rand )
{
case 0: default: enemyType = EnemyType.MinibossAbsorber; break;
case 1: enemyType = EnemyType.MinibossCharger; break;
case 2: enemyType = EnemyType.MinibossCharger2; break;
case 3: enemyType = EnemyType.MinibossExploder; break;
case 4: enemyType = EnemyType.MinibossRunner; break;
case 5: enemyType = EnemyType.MinibossShielder; break;
case 6: enemyType = EnemyType.MinibossSlammer; break;
case 7: enemyType = EnemyType.MinibossSniper; break;
case 8: enemyType = EnemyType.MinibossSpiker; break;
case 9: enemyType = EnemyType.MinibossSpiker2; break;
case 10: enemyType = EnemyType.MinibossSpitter; break;
case 11: enemyType = EnemyType.MinibossTroll; break;
case 12: enemyType = EnemyType.MinibossZapper; break;
case 13: enemyType = EnemyType.MinibossZapper2; break;
case 14: enemyType = EnemyType.MinibossZoner; break;
}
SpawnEnemy( enemyType, pos, spawnInstantly );
}
[Rpc.Broadcast]
public void SpawnSnaker( Vector2 pos, int numSegments )
{
var cloudPos = new Vector3( pos.x + Game.Random.Float( -15f, 15f ), pos.y + Game.Random.Float( -15f, 15f ), 15f );
GameObject.Clone( "prefabs/effects/enemy_spawn_clouds.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( cloudPos ) } );
PlaySfxNearby( "zombie.dirt", pos, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f, maxDist: 350f );
Snaker curr, prev = null;
Vector2 startPos = new Vector2( Game.Random.Float( -300f, 300f ), Game.Random.Float( -300f, 300f ) );
float angle = Game.Random.Float( 0f, 360f );
Vector2 angleVec = Utils.GetVector2FromAngleDegrees( angle );
for ( int i = 0; i < numSegments; i++ )
{
curr = SpawnSnakerSegment( pos - angleVec * 25f * i, angle );
if ( prev != null )
curr.AssignLeader( prev );
curr.NumSegments = curr.NumSegmentsStart = numSegments;
prev = curr;
}
}
public Snaker SpawnSnakerSegment( Vector2 pos, float rotAngle = 0f )
{
pos = ClampPosToBounds( pos );
//if ( !spawnInstantly )
//{
// var cloudPos = new Vector3( pos.x + Game.Random.Float( -15f, 15f ), pos.y + Game.Random.Float( -15f, 15f ), 10f );
// GameObject.Clone( "prefabs/effects/enemy_spawn_clouds.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( cloudPos ) } );
// if ( playSfx )
// PlaySfxNearby( "zombie.dirt", pos, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f, maxDist: 350f );
//}
var zPos = -100f;
var yaw = MathF.Abs( rotAngle ) > 0f ? rotAngle : Game.Random.Float( 0f, 360f );
var enemyGo = GameObject.Clone( $"prefabs/enemies/snaker.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( position: new Vector3( pos.x, pos.y, zPos ), rotation: new Angles( 0f, yaw, 0f ) ) } );
var enemy = enemyGo.GetComponent<Enemy>();
enemyGo.NetworkSpawn();
NumEnemies++;
return enemy as Snaker;
}
[Rpc.Host]
public void SpawnCoinRpc( Vector2 pos, int value, Vector2 dir, Player magnetizePlayer = null )
{
SpawnCoin( pos, value, dir, magnetizePlayer );
}
public void SpawnCoin( Vector2 pos, int value, Vector2 dir, Player magnetizePlayer = null )
{
Assert.True( Networking.IsHost );
var coinGo = GameObject.Clone( "prefabs/items/coin.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 0f ) ) } );
var coin = coinGo.GetComponent<Coin>();
coin.Velocity = dir * Game.Random.Float( ITEM_SPAWN_VELOCITY_MIN, ITEM_SPAWN_VELOCITY_MAX );
coin.SetValue( value );
coin.Fly(
targetPos: pos + dir * Game.Random.Float( 30f, 70f ),
time: Game.Random.Float( 0.4f, 0.55f ),
height: Game.Random.Float( 50f, 120f )
);
coinGo.NetworkSpawn();
if ( magnetizePlayer.IsValid() )
coin.MagnetizeRpc( magnetizePlayer );
}
[Rpc.Host]
public void SpawnCoinRpc( Vector2 pos, int value, Player targetPlayer, float time, float height )
{
SpawnCoin( pos, value, targetPlayer, time, height );
}
public void SpawnCoin( Vector2 pos, int value, Player targetPlayer, float time, float height )
{
Assert.True( Networking.IsHost );
var coinGo = GameObject.Clone( "prefabs/items/coin.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 0f ) ) } );
var coin = coinGo.GetComponent<Coin>();
coin.FlyToPlayer( targetPlayer, time, height );
var dir = targetPlayer.IsValid
? (targetPlayer.Position2D - pos).Normal
: Utils.GetRandomVector();
coin.Velocity = dir * Game.Random.Float( ITEM_SPAWN_VELOCITY_MIN, ITEM_SPAWN_VELOCITY_MAX );
coin.SetValue( value );
coinGo.NetworkSpawn();
}
// todo: needs to support zPos, so if player spawns a perk item while in the air, it'll look right
[Rpc.Host]
public void SpawnPerkItemRpc( int typeIdentity, Vector2 pos, Vector2 dir, Player playerToTarget = null )
{
SpawnPerkItem( PerkManager.IdentityToType( typeIdentity ), pos, dir, playerToTarget );
}
[Rpc.Host]
public void SpawnPerkItemAtRpc( int typeIdentity, Vector2 startPos, Vector2 endPos, Player playerToTarget = null )
{
SpawnPerkItemAt( PerkManager.IdentityToType( typeIdentity ), startPos, endPos, playerToTarget );
}
public void SpawnPerkItemAt( TypeDescription perkType, Vector2 startPos, Vector2 endPos, Player playerToTarget = null )
{
Assert.True( Networking.IsHost );
var perkGo = GameObject.Clone( "prefabs/items/perk_item.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( startPos.x, startPos.y, 0f ) ) } );
var perkItem = perkGo.GetComponent<PerkItem>();
var dist = (endPos - startPos).Length;
perkItem.Fly(
targetPos: endPos,
time: Math.Clamp( dist / 600f, 0.4f, 1.5f ),
height: Game.Random.Float( dist * 0.4f, dist * 0.6f )
);
perkItem.PlayerToTarget = playerToTarget;
perkGo.NetworkSpawn();
perkItem.Setup( PerkManager.TypeToIdentity( perkType ) );
}
public void SpawnPerkItem( TypeDescription perkType, Vector2 pos, Vector2 dir, Player playerToTarget = null )
{
Assert.True( Networking.IsHost );
var perkGo = GameObject.Clone( "prefabs/items/perk_item.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 0f ) ) } );
var perkItem = perkGo.GetComponent<PerkItem>();
perkItem.Velocity = dir * Game.Random.Float( ITEM_SPAWN_VELOCITY_MIN, ITEM_SPAWN_VELOCITY_MAX );
perkItem.Fly(
targetPos: pos + dir * Game.Random.Float( 60f, 80f ),
time: Game.Random.Float( 0.4f, 0.55f ),
height: Game.Random.Float( 50f, 120f )
);
perkItem.PlayerToTarget = playerToTarget;
perkGo.NetworkSpawn();
perkItem.Setup( PerkManager.TypeToIdentity( perkType ) );
}
public Boomerang SpawnBoomerang( Vector2 pos, Vector2 velocity, float damage, Player shooter )
{
var boomerangGo = GameObject.Clone( "prefabs/boomerang.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 40f ) ) } );
var boomerang = boomerangGo.Components.Get<Boomerang>( true );
boomerang.Velocity = velocity;
boomerang.Damage = damage;
boomerang.Shooter = shooter;
boomerangGo.NetworkSpawn( shooter.Network.Owner );
return boomerang;
}
[Rpc.Host]
public void SpawnItemRpc( string name, Vector2 pos, Vector2 dir, bool fly = true, bool addVel = true )
{
SpawnItem( name, pos, dir );
}
public void SpawnItem( string name, Vector2 pos, Vector2 dir, bool fly = true, bool addVel = true )
{
Assert.True( Networking.IsHost );
var itemGo = GameObject.Clone( $"prefabs/items/{name}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, -100f ) ) } );
var item = itemGo.GetComponent<Item>();
if ( dir.LengthSquared > 0f )
{
if ( addVel )
item.Velocity = dir * Game.Random.Float( ITEM_SPAWN_VELOCITY_MIN, ITEM_SPAWN_VELOCITY_MAX );
if ( fly )
{
item.Fly(
targetPos: pos + dir * Game.Random.Float( 70f, 120f ),
time: Game.Random.Float( 0.4f, 0.55f ),
height: Game.Random.Float( 50f, 120f )
);
}
}
if ( name == "magnet" )
TimeSinceMagnet = 0f;
itemGo.NetworkSpawn();
}
[Rpc.Host]
public void SpawnItemFlyToRpc( string name, Vector2 spawnPos, Vector2 flyTargetPos, float time, float height)
{
var itemGo = GameObject.Clone( $"prefabs/items/{name}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( spawnPos.x, spawnPos.y, -100f ) ) } );
var item = itemGo.GetComponent<Item>();
item.Fly( flyTargetPos, time, height );
itemGo.NetworkSpawn();
}
[Rpc.Host]
public void SpawnItemRpc( string name, Vector2 pos, Player targetPlayer, float time, float height )
{
SpawnItem( name, pos, targetPlayer, time, height );
}
public void SpawnItem( string name, Vector2 pos, Player targetPlayer, float time, float height )
{
Assert.True( Networking.IsHost );
var itemGo = GameObject.Clone( $"prefabs/items/{name}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, -100f ) ) } );
var item = itemGo.GetComponent<Item>();
item.FlyToPlayer( targetPlayer, time, height );
var dir = (targetPlayer.Position2D - pos).Normal;
item.Velocity = dir * Game.Random.Float( ITEM_SPAWN_VELOCITY_MIN, ITEM_SPAWN_VELOCITY_MAX );
itemGo.NetworkSpawn();
}
[Rpc.Broadcast]
public void SpawnBulletImpactParticlesRpc( Vector3 pos, Vector3 normal, Color color, float scaleMultiplier = 1f )
{
SpawnBulletImpactParticles( pos, normal, color, scaleMultiplier );
}
public void SpawnBulletImpactParticles( Vector3 pos, Vector3 normal, Color color, float scaleMultiplier = 1f )
{
var particleGo = GameObject.Clone( "prefabs/effects/bullet_impact.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
var particleEffect = particleGo.GetComponent<ParticleEffect>();
particleEffect.Tint = color;
var particleSpriteRenderer = particleGo.GetComponent<ParticleSpriteRenderer>();
particleSpriteRenderer.Scale *= scaleMultiplier;
//PlaySfxNearbyRpc( "bullet.impact", (Vector2)pos, pitch: Game.Random.Float(1.05f, 1.15f), volume: 0.7f, maxDist: 120f );
}
public void SpawnDamageNumber( Vector3 pos, float damage, Color color, float size, FloaterType floaterType = FloaterType.Damage, DamageResultFlags damageFlags = DamageResultFlags.None )
{
string str;
if ( ShowPreciseNumbers )
{
str = damage % 1 == 0 ? damage.ToString( "0" ) : damage.ToString( "0.#" );
}
else
{
int amount = GetFloaterNumber( damage );
str = amount.ToString();
}
str += GetDamageResultExtraText( damageFlags );
SpawnFloaterText( pos, str, color, size, floaterType, damageFlags );
}
public static string GetDamageResultExtraText( DamageResultFlags flags )
{
string extraText = "";
if ( flags.HasFlag( DamageResultFlags.AlternateDmg ) )
extraText += "!";
if ( flags.HasFlag( DamageResultFlags.CoupDeGrace ) )
extraText += "!";
return extraText;
}
public int GetFloaterNumber( float amount )
{
if ( amount < 1f )
{
amount = MathF.Ceiling( amount );
}
else
{
float fractional = amount - MathF.Floor( amount );
if ( fractional > 0f && Game.Random.Float( 0f, 1f ) > fractional ) // the higher the fractional, the lower the chance of rolling higher than it, so Floor when doing so
amount = MathF.Floor( amount );
else
amount = MathF.Ceiling( amount );
}
return (int)amount;
}
[Rpc.Broadcast]
public void SpawnFloaterNumberRpc( Vector3 pos, float amount, Color color, float size, FloaterType floaterType = FloaterType.Damage, DamageResultFlags damageFlags = DamageResultFlags.None )
{
SpawnFloaterNumber( pos, amount, color, size, floaterType, damageFlags );
}
public void SpawnFloaterNumber( Vector3 pos, float amount, Color color, float size, FloaterType floaterType = FloaterType.Damage, DamageResultFlags damageFlags = DamageResultFlags.None )
{
if ( ShowPreciseNumbers )
{
string str = amount % 1 == 0 ? amount.ToString( "0" ) : amount.ToString( "0.#" );
SpawnFloaterText( pos, str, color, size, floaterType, damageFlags );
return;
}
SpawnFloaterText( pos, GetFloaterNumber( amount ).ToString(), color, size, floaterType, damageFlags );
}
[Rpc.Broadcast]
public void SpawnFloaterTextRpc( Vector3 pos, string message, Color color, float size, FloaterType floaterType = FloaterType.Damage, DamageResultFlags damageFlags = DamageResultFlags.None )
{
SpawnFloaterText( pos, message, color, size, floaterType, damageFlags );
}
public void SpawnFloaterText( Vector3 pos, string message, Color color, float size, FloaterType floaterType = FloaterType.Damage, DamageResultFlags damageFlags = DamageResultFlags.None )
{
if ( color == default )
color = Color.White;
if ( IsOrthoCamera )
size *= 1.2f;
var localPlayer = Manager.Instance.LocalPlayer;
bool fpsMode = localPlayer.IsValid() && localPlayer.GetSyncStat( PlayerStat.FpsMode ) > 0f;
if ( !fpsMode )
{
// Bring floater toward camera, but ignore x-pos so it doesn't get clamped near arena edges.
pos += (CameraContainer.WorldPosition - pos).Normal.WithX( 0f ) * 100f;
}
else
{
// FPS camera: just nudge upward slightly so numbers appear above the hit point.
pos += Vector3.Up * 20f;
}
var particle = GameObject.Clone( "prefabs/dmg_number.prefab", new CloneConfig()
{
Transform = new Transform( pos, new Angles( -90f, 0, 0 ) ),
StartEnabled = true
} );
var particleEffect = particle.Components.Get<ParticleEffect>();
var textRenderer = particle.Components.Get<ParticleTextRenderer>();
//particleEffect.Timing = IsPaused ? ParticleEffect.TimingMode.GameTime : ParticleEffect.TimingMode.RealTime;
particleEffect.Timing = ParticleEffect.TimingMode.RealTime;
if ( floaterType == FloaterType.PoisonFinish )
message += "!";
if ( textRenderer != null )
{
var text = new TextRendering.Scope( message, color, 250f, weight: 800 );
text.Outline.Enabled = true;
text.Outline.Color = Color.Black;
text.Outline.Size = 50;
if ( floaterType == FloaterType.Poison )
{
text.Outline.Color = new Color( 0f, 0.25f, 0f );
text.Outline.Size = 60;
}
else if ( floaterType == FloaterType.PoisonFinish )
{
text.Outline.Color = new Color( 0f, 0.25f, 0f );
text.Outline.Size = 60;
text.FontItalic = true;
size *= 1.2f;
}
else if ( floaterType == FloaterType.Shock )
{
//text.Outline.Color = new Color( 0.4f, 0.4f, 0f );
//text.Outline.Size = 70;
//size *= 1.1f;
text.Shadow.Enabled = true;
text.Shadow.Size = 20f;
text.Shadow.Color = Color.Yellow;
text.Shadow.Offset = new Vector2( 0f, 50f );
}
else if ( floaterType == FloaterType.Fire )
{
text.Outline.Color = new Color( 0.7f, 0f, 0f );
text.Outline.Size = 70;
}
else if ( floaterType == FloaterType.FrostDmg )
{
text.Outline.Color = new Color( 0f, 0f, 0.5f );
text.Outline.Size = 70;
}
else if ( floaterType == FloaterType.OrbiterBlade )
{
text.Outline.Color = new Color( 0.45f, 0.45f, 0.45f );
text.Outline.Size = 70;
//text.FontItalic = true;
}
else if ( floaterType == FloaterType.Thorns )
{
text.Outline.Color = new Color( 0f, 0.25f, 0f );
text.Outline.Size = 40;
text.Shadow.Enabled = true;
text.Shadow.Size = 20f;
text.Shadow.Color = new Color(0f, 0.4f, 0f);
text.Shadow.Offset = new Vector2( 0f, -50f );
}
else if ( floaterType == FloaterType.Backstab )
{
text.Outline.Color = new Color( 0.4f, 0.2f, 0f );
text.Outline.Size = 60;
text.FontItalic = true;
size *= 1.1f;
}
else if ( floaterType == FloaterType.DodgeHpDamage )
{
text.Shadow.Enabled = true;
text.Shadow.Size = 30f;
text.Shadow.Color = new Color( 0.8f, 0f, 0f );
//text.Shadow.Offset = new Vector2( 0f, -50f );
//text.FontItalic = true;
size *= 1.15f;
}
else if ( floaterType == FloaterType.Radiation )
{
text.Outline.Color = new Color( 0.2f, 0.35f, 0f );
text.Outline.Size = 60;
text.Shadow.Enabled = true;
text.Shadow.Size = 45f;
text.Shadow.Color = new Color( 0.5f, 0.9f, 0.1f );
text.Shadow.Offset = new Vector2( 0f, 40f );
}
else if ( floaterType == FloaterType.Mark )
{
text.Outline.Color = new Color( 0.2f, 0f, 0.4f );
text.Outline.Size = 65;
text.Shadow.Enabled = true;
text.Shadow.Size = 40f;
text.Shadow.Color = new Color( 0.6f, 0.1f, 1f );
text.Shadow.Offset = new Vector2( 0f, -40f );
}
if ( damageFlags.HasFlag( DamageResultFlags.FullHealthEnemy ) )
{
text.Outline.Size = text.Outline.Size + 50;
text.FontWeight = 900;
}
if ( damageFlags.HasFlag( DamageResultFlags.PoisonIncreaseNearby ) )
{
text.Outline.Size = text.Outline.Size + 20;
text.FontWeight = 900;
text.Outline.Color = new Color( 0f, 0.6f, 0.4f );
text.Shadow.Enabled = true;
text.Shadow.Size = 45f;
text.Shadow.Color = new Color( 0f, 0.6f, 0.4f );
text.Shadow.Offset = new Vector2( 0f, 50f );
}
//text.WordSpacing = 100f;
//text.LetterSpacing = -50f;
textRenderer.Text = text;
textRenderer.Scale = size * 10f;
}
if ( floaterType == FloaterType.Heal )
{
particleEffect.StartVelocity = Game.Random.Float( 20f, 40f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, 50f );
particleEffect.Damping = 0.35f;
particleEffect.Stretch = 0f;
}
else if ( floaterType == FloaterType.Xp )
{
particleEffect.StartVelocity = Game.Random.Float( 20f, 40f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, 30f );
particleEffect.Damping = 0.2f;
particleEffect.Stretch = 0f;
particleEffect.Lifetime = 0.66f;
particleEffect.Scale = 1f;
}
else if ( floaterType == FloaterType.XpLoss )
{
particleEffect.StartVelocity = Game.Random.Float( -20f, -40f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, -30f );
particleEffect.Damping = 0.2f;
particleEffect.Stretch = 0f;
particleEffect.Lifetime = 0.66f;
particleEffect.Scale = 1f;
}
else if ( floaterType == FloaterType.PositiveMessage )
{
particleEffect.StartVelocity = Game.Random.Float( 20f, 45f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, 10f );
particleEffect.Damping = 0.2f;
particleEffect.Stretch = 0f;
particleEffect.Scale = 1.05f;
}
else if ( floaterType == FloaterType.NegativeMessage )
{
particleEffect.StartVelocity = Game.Random.Float( -20f, -30f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, -50f );
particleEffect.Damping = 0.2f;
particleEffect.Stretch = 0f;
particleEffect.Scale = 1f;
}
else if ( floaterType == FloaterType.Dodge )
{
particleEffect.StartVelocity = Game.Random.Float( 50f, 60f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, 30f );
particleEffect.Damping = 0.1f;
particleEffect.Stretch = 0f;
particleEffect.Lifetime = 0.3f;
}
else if ( floaterType == FloaterType.ArmorGain )
{
particleEffect.StartVelocity = Game.Random.Float( 40f, 60f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, 50f );
particleEffect.Damping = 0.45f;
particleEffect.Stretch = 0f;
}
else if ( floaterType == FloaterType.ArmorLose )
{
particleEffect.StartVelocity = Game.Random.Float( 10f, 20f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, 0f );
particleEffect.Damping = 0.85f;
particleEffect.Stretch = 0f;
}
else if ( floaterType == FloaterType.Reload1 )
{
particleEffect.StartVelocity = Game.Random.Float( 50f, 60f );
particleEffect.ForceDirection = new Vector3( 0f, 0f, 30f );
particleEffect.Damping = 1.5f;
particleEffect.Stretch = 0f;
particleEffect.Lifetime = 0.3f;
}
}
[Rpc.Broadcast]
public void SpawnPerkFloaterRpc( Vector3 pos, string iconPath, Rarity rarity, bool curse, float progress, bool wasRemoved = false )
{
SpawnPerkFloater( pos, iconPath, rarity, curse, progress, wasRemoved );
}
public void SpawnPerkFloater( Vector3 pos, string iconPath, Rarity rarity, bool curse, float progress, bool wasRemoved = false )
{
var floaterGo = GameObject.Clone( "prefabs/perk_floater.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
var perkFloater = floaterGo.GetComponent<PerkFloater>();
perkFloater.IconPath = iconPath;
perkFloater.Rarity = rarity;
perkFloater.Curse = curse;
perkFloater.Progress = progress;
perkFloater.WasRemoved = wasRemoved;
perkFloater.Timing = ParticleEffect.TimingMode.RealTime;
if ( wasRemoved )
perkFloater.RemovedRotateSpeed = Game.Random.Float( 8f, 30f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
perkFloater.Velocity = wasRemoved
? new Vector3( Game.Random.Float( -40f, 40f ), 0f, -150f )
: new Vector3( Game.Random.Float( -40f, 40f ), 0f, 150f );
}
public const float RING_HEIGHT = 10f;
public const float SCORCH_HEIGHT = 1f;
[Rpc.Broadcast]
public void SpawnRingRpc( Vector2 pos, float radius, Color color, float lifetime, string path = "ring", bool useRealtime = false )
{
SpawnRing( pos, radius, color, lifetime, path, useRealtime );
}
public void SpawnRing( Vector2 pos, float radius, Color color, float lifetime, string path = "ring", bool useRealtime = false )
{
var zPos = RING_HEIGHT * Game.Random.Float( 0.95f, 1.05f );
var particleGo = GameObject.Clone( $"prefabs/effects/{path}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ), new Angles( 90f, Game.Random.Float( 0f, 360f ), 0f ) ) } );
var particleEffect = particleGo.GetComponent<ParticleEffect>();
particleEffect.Tint = color;
particleEffect.Lifetime = lifetime;
var particleSpriteRenderer = particleGo.GetComponent<ParticleSpriteRenderer>();
particleSpriteRenderer.Scale = radius;
particleEffect.Timing = useRealtime ? ParticleEffect.TimingMode.RealTime : ParticleEffect.TimingMode.GameTime;
}
//[Rpc.Broadcast]
//public void SpawnParticleRpc( string name, Vector3 pos )
//{
// GameObject.Clone( $"prefabs/effects/{name}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
//}
public void SpawnParticle( string name, Vector3 pos )
{
GameObject.Clone( $"prefabs/effects/{name}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
}
[Rpc.Host]
public void SpawnAcidPuddleRpc( Vector2 pos, float lifetime, float damage, float scale, Color colorA, Color colorB, Player playerSource, Enemy enemySource, EnemyType enemyType, bool damagePlayers = true, bool damageEnemies = false )
{
SpawnAcidPuddle( pos, lifetime, damage, scale, colorA, colorB, playerSource, enemySource, enemyType, damagePlayers, damageEnemies );
}
public void SpawnAcidPuddle( Vector2 pos, float lifetime, float damage, float scale, Color colorA, Color colorB, Player playerSource, Enemy enemySource, EnemyType enemyType, bool damagePlayers = true, bool damageEnemies = false )
{
var puddleGo = GameObject.Clone( $"prefabs/acid_puddle.prefab", new CloneConfig
{
StartEnabled = true,
Transform = new Transform(
position: new Vector3( pos.x, pos.y, 2f ),
rotation: new Angles( 0f, Game.Random.Float( 0f, 360f ), 0f ),
scale: new Vector3( scale )
)
} );
var puddle = puddleGo.GetComponent<AcidPuddle>();
puddle.Lifetime = lifetime;
puddle.Damage = damage;
puddle.ColorA = colorA;
puddle.ColorB = colorB;
puddle.EnemySource = enemySource;
puddle.EnemyType = enemyType;
puddle.PlayerSource = playerSource;
if ( damagePlayers )
puddle.CollideWithTags.Add( "player" );
if ( damageEnemies )
puddle.CollideWithTags.Add( "enemy" );
puddleGo.NetworkSpawn();
}
[Rpc.Host]
public void SpawnAcidPuddleRingRpc( Vector2 centerPos, float radius, int num, float scale, float lifetime, float damage, Player playerSource, Enemy enemySource, EnemyType enemyType, float startDegrees = 0f, bool damagePlayers = true, bool damageEnemies = false )
{
float interval = 360f / num;
float degrees = startDegrees;
for ( int i = 0; i < num; i++ )
{
var pos = centerPos + Utils.GetVector2FromAngleDegrees( degrees ) * radius;
if ( IsInBounds( pos ) )
{
SpawnAcidPuddle( pos, lifetime * Game.Random.Float( 0.95f, 1.05f ), damage, scale, colorA: new Color( 1f, 0.3f, 0.4f ), colorB: new Color( 1f, 0.5f, 0.6f ), playerSource, enemySource, enemyType, damagePlayers, damageEnemies );
}
degrees += interval;
}
}
[Rpc.Host]
public void SpawnFireGroundRpc( Vector2 pos, Player player, Enemy enemySource, EnemyType enemyType, float damage, float lifetime, float spreadChance, bool canStack, float scale, Color colorA, Color colorB, bool hurtPlayers = true, bool hurtEnemies = true )
{
SpawnFireGround( pos, player, enemySource, enemyType, damage, lifetime, spreadChance, canStack, scale, colorA, colorB, hurtPlayers, hurtEnemies );
}
public void SpawnFireGround( Vector2 pos, Player player, Enemy enemySource, EnemyType enemyType, float damage, float lifetime, float spreadChance, bool canStack, float scale, Color colorA, Color colorB, bool hurtPlayers = true, bool hurtEnemies = true )
{
var fireGo = GameObject.Clone( $"prefabs/fire_ground.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 10f ) ) } );
var fire = fireGo.GetComponent<FireGround>();
fire.PlayerSource = player;
fire.EnemySource = enemySource;
fire.EnemyType = enemyType;
fire.Lifetime = lifetime;
fire.Damage = damage;
fire.SpreadChance = spreadChance;
fire.CanStack = canStack;
fire.HurtPlayers = hurtPlayers;
fire.HurtEnemies = hurtEnemies;
fire.SetScale( scale );
var particleEffect = fire.GetComponent<ParticleEffect>();
particleEffect.Gradient = new ParticleGradient() { Type = ParticleGradient.ValueType.Range, ConstantA = colorA, ConstantB = colorB };
fireGo.NetworkSpawn();
}
[Rpc.Host]
public void SpawnFireRingRpc( Vector2 centerPos, float radius, int num, float damage, float lifetime, float startDegrees, float scale, Color colorA, Color colorB, Player playerSource, Enemy enemySource, EnemyType enemyType, bool hurtPlayers = true, bool hurtEnemies = true )
{
SpawnFireRing( centerPos, radius, num, damage, lifetime, startDegrees, scale, colorA, colorB, playerSource, enemySource, enemyType, hurtPlayers, hurtEnemies );
}
public void SpawnFireRing( Vector2 centerPos, float radius, int num, float damage, float lifetime, float startDegrees, float scale, Color colorA, Color colorB, Player playerSource, Enemy enemySource, EnemyType enemyType, bool hurtPlayers = true, bool hurtEnemies = true, float spreadChance = 0f )
{
float interval = 360f / num;
float degrees = startDegrees;
for ( int i = 0; i < num; i++ )
{
var pos = centerPos + Utils.GetVector2FromAngleDegrees( degrees ) * radius;
if ( IsInBounds( pos ) )
{
SpawnFireGround( pos, playerSource, enemySource, enemyType, damage, lifetime * Game.Random.Float( 0.95f, 1.05f ), spreadChance: spreadChance, canStack: false, scale, colorA, colorB, hurtPlayers, hurtEnemies );
}
degrees += interval;
}
PlaySfxNearbyRpc( "burn", centerPos, pitch: Game.Random.Float( 0.95f, 1f ), volume: 0.9f, maxDist: 400f );
}
[Rpc.Host]
public void SpawnEnemyRingRpc( EnemyType enemyType, Vector2 centerPos, float radius, int num, float startDegrees = 0f )
{
float interval = 360f / num;
float degrees = startDegrees;
for ( int i = 0; i < num; i++ )
{
var pos = centerPos + Utils.GetVector2FromAngleDegrees( degrees ) * radius;
if ( IsInBounds( pos ) )
{
SpawnEnemy( enemyType, pos, spawnInstantly: false, rotAngle: Utils.GetAngleDegreesFromVectorAlt( (centerPos - pos).Normal ), playSfx: false );
}
degrees += interval;
}
PlaySfxNearbyRpc( "zombie.dirt", centerPos, pitch: Game.Random.Float( 0.6f, 0.7f ), volume: 0.8f, maxDist: 450f );
}
[Rpc.Host]
public void SpawnBombRingRpc( Vector2 centerPos, float radius, int num, float startDegrees = 0f )
{
float interval = 360f / num;
float degrees = startDegrees;
for ( int i = 0; i < num; i++ )
{
var pos = centerPos + Utils.GetVector2FromAngleDegrees( degrees ) * radius;
if ( IsInBounds( pos ) )
{
SpawnItem( "bomb", pos, (centerPos - pos).Normal, fly: false );
SpawnCloudRpc( pos: new Vector3( pos.x, pos.y, 10f ), velocity: Vector2.Zero, deceleration: 4f );
}
degrees += interval;
}
}
[Rpc.Host]
public void SpawnLandmineRingRpc( Vector2 centerPos, float radius, int num, float startDegrees = 0f )
{
float interval = 360f / num;
float degrees = startDegrees;
for ( int i = 0; i < num; i++ )
{
var pos = centerPos + Utils.GetVector2FromAngleDegrees( degrees ) * radius;
if ( IsInBounds( pos ) )
{
var landmineGo = GameObject.Clone( "prefabs/landmine.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 0f ) ) } );
var landmine = landmineGo.Components.Get<Landmine>( true );
landmine.Damage = 15;
landmine.Shooter = null;
landmineGo.NetworkSpawn();
// todo: cant really see this cloud... give a better effect when spawning landmine
SpawnCloudRpc( pos: new Vector3( pos.x, pos.y, 10f ), velocity: Vector2.Zero, deceleration: 4f );
}
degrees += interval;
}
}
[Rpc.Host]
public void SpawnEnemyProjectileRing( Vector2 centerPos, int numProjectiles, Enemy shooter, EnemyType enemyType, float startVelMin, float startVelMax, float lifetimeModMin, float lifetimeModMax, EnemyProjectileType projectileType = EnemyProjectileType.Normal, float zPos = 50f )
{
var dir = Utils.GetRandomVector();
float increment = 360f / numProjectiles;
for ( int i = 0; i < numProjectiles; i++ )
{
var currDir = Utils.RotateVector( dir, increment * i );
var pos = centerPos + currDir * 20f;
var startVel = Game.Random.Float( startVelMin, startVelMax );
var lifetimeMod = Game.Random.Float( lifetimeModMin, lifetimeModMax );
SpawnEnemyProjectile( pos, currDir, shooter, enemyType, startVel, projectileType, zPos, lifetimeMod );
}
}
[Rpc.Host]
public void SpawnEnemyHomingProjectileRing( Vector2 centerPos, int numProjectiles, Enemy shooter, EnemyType enemyType, float startVelMin, float startVelMax, float lifetimeModMin, float lifetimeModMax, EnemyProjectileType projectileType = EnemyProjectileType.Normal, float zPos = 50f )
{
var dir = Utils.GetRandomVector();
float increment = 360f / numProjectiles;
for ( int i = 0; i < numProjectiles; i++ )
{
var currDir = Utils.RotateVector( dir, increment * i );
var pos = centerPos + currDir * 20f;
var startVel = Game.Random.Float( startVelMin, startVelMax );
var lifetimeMod = Game.Random.Float( lifetimeModMin, lifetimeModMax );
SpawnEnemyHomingProjectile( pos, currDir, shooter, enemyType, startVel, projectileType, zPos, lifetimeMod );
}
}
public void SpawnCloud( Vector3 pos, Vector2 velocity, float deceleration, float lifetime, bool bright = false )
{
var cloudGo = GameObject.Clone( $"prefabs/effects/{(bright ? "cloud_bright" : "cloud")}.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
var cloud = cloudGo.GetComponent<Cloud>();
cloud.Velocity = velocity;
cloud.Deceleration = deceleration;
cloud.GetComponent<ParticleEffect>().Lifetime = lifetime;
}
public void SpawnCloud( Vector3 pos, Vector2 velocity, float deceleration, bool bright = false )
{
var lifetime = Game.Random.Float( 1.5f, 2.5f );
//var zPos = 0f;
SpawnCloud( pos, velocity, deceleration, lifetime, bright );
}
[Rpc.Broadcast]
public void SpawnCloudRpc( Vector3 pos, Vector2 velocity, float deceleration, float lifetime, bool bright = false )
{
SpawnCloud( pos, velocity, deceleration, lifetime, bright );
}
[Rpc.Broadcast]
public void SpawnCloudRpc( Vector3 pos, Vector2 velocity, float deceleration, bool bright = false )
{
SpawnCloud( pos, velocity, deceleration, bright );
}
public const float SHOCKWAVE_HEIGHT = 10f;
public void SpawnShockwave( Vector2 pos, float damage, float radius, float lifetime, float force, Gradient gradient, Enemy enemySource, EnemyType enemyType )
{
var shockwaveGo = GameObject.Clone( "prefabs/shockwave.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, SHOCKWAVE_HEIGHT ) ) } );
var shockwave = shockwaveGo.GetComponent<Shockwave>();
shockwave.Damage = damage;
shockwave.Radius = radius;
shockwave.Lifetime = lifetime;
shockwave.Force = force;
shockwave.Gradient = gradient;
shockwave.EnemySource = enemySource;
shockwave.EnemyType = enemyType;
}
public bool GetLavaBlobEndPos( Vector2 startPos, out Vector2 endPos )
{
endPos = ClampPosToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 0f, 500f ), buffer: 40f );
int NUM_TRIES = 40;
int count = 0;
while ( IsInLava( endPos ) && count < NUM_TRIES )
{
endPos = ClampPosToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 0f, 500f ), buffer: 40f );
count++;
}
count = 0;
while ( IsInLava( endPos ) && count < NUM_TRIES )
{
endPos = ClampPosToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 5f, 650f ), buffer: 40f );
count++;
}
count = 0;
while ( IsInLava( endPos ) && count < NUM_TRIES )
{
endPos = ClampPosToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 6.5f, 750f ), buffer: 40f );
count++;
}
return !IsInLava( endPos );
}
public bool IsInLava( Vector2 pos )
{
float radiusIncrease = 1.3f;
foreach ( var lavaPuddle in Scene.GetAllComponents<LavaPuddle>() )
{
if ( lavaPuddle.TimeSinceSpawn > lavaPuddle.Lifetime - 0.85f )
continue;
if ( ((Vector2)lavaPuddle.WorldPosition - pos).LengthSquared < MathF.Pow( lavaPuddle.Radius * radiusIncrease, 2f ) )
return true;
}
foreach ( var lavaBlob in Scene.GetAllComponents<TossedLavaBlob>() )
{
if ( (lavaBlob.TargetPos2D - pos).LengthSquared < MathF.Pow( lavaBlob.PuddleRadius * radiusIncrease, 2f ) )
return true;
}
foreach ( var spikerHeadMiniboss2 in Scene.GetAllComponents<SpikerHeadMiniboss2>() )
{
if ( (spikerHeadMiniboss2.Position2D - pos).LengthSquared < MathF.Pow( 160f * radiusIncrease, 2f ) )
return true;
}
foreach ( var particles in Scene.FindAllWithTag( "spiker_miniboss_2_particles" ) )
{
if ( ((Vector2)particles.WorldPosition - pos).LengthSquared < MathF.Pow( 160f * radiusIncrease, 2f ) )
return true;
}
return false;
}
[Rpc.Broadcast]
public void SpawnLavaBlob( Vector2 pos, Vector2 targetPos, float puddleRadius, Enemy enemySource, EnemyType enemyType )
{
PlaySfxNearby( "lava_blob_01", pos, pitch: Game.Random.Float( 0.9f, 1.1f ), volume: 1.4f, maxDist: 550f );
GameObject.Clone( "prefabs/effects/lava_warning.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( targetPos.x, targetPos.y, 0f ) ) } );
if ( IsProxy )
return;
var zPos = 50f;
var blobGo = GameObject.Clone( "prefabs/enemyProjectiles/tossed_lava_blob.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ), Rotation.Random ) } );
var blob = blobGo.Components.Get<TossedLavaBlob>( true );
blob.Shooter = enemySource;
blob.EnemyType = enemyType;
blob.Lifetime = Game.Random.Float( 1.75f, 2.25f );
blob.StartPos2D = pos;
blob.TargetPos2D = Manager.Instance.ClampPosToBounds( targetPos );
blob.PuddleRadius = puddleRadius;
blobGo.NetworkSpawn();
}
[Rpc.Broadcast]
public void SpawnLavaPuddleRpc( Vector2 pos, float damage, float radius, float lifetime, Color colorA, Color colorB, float alphaMax, Enemy enemySource, EnemyType enemyType )
{
SpawnLavaPuddle( pos, damage, radius, lifetime, colorA, colorB, alphaMax, enemySource, enemyType );
}
public void SpawnLavaPuddle( Vector2 pos, float damage, float radius, float lifetime, Color colorA, Color colorB, float alphaMax, Enemy enemySource, EnemyType enemyType )
{
var lavaPuddleGo = GameObject.Clone( "prefabs/lava_puddle.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 1f ), new Angles(0f, Game.Random.Float(0f, 360f), 0f) ) } );
var lavaPuddle = lavaPuddleGo.GetComponent<LavaPuddle>();
lavaPuddle.Damage = damage;
lavaPuddle.Radius = radius;
lavaPuddle.Lifetime = lifetime;
lavaPuddle.ColorA = colorA;
lavaPuddle.ColorB = colorB;
lavaPuddle.AlphaMax = alphaMax;
lavaPuddle.EnemySource = enemySource;
lavaPuddle.EnemyType = enemyType;
}
[Rpc.Host]
public void SpawnLavaSafeSpot( Vector2 pos, float radius, Vector2 direction )
{
var safeSpotGo = GameObject.Clone( "prefabs/lava_safe_spot.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, -99f ) ) } );
var safeSpot = safeSpotGo.GetComponent<LavaSafeSpot>();
safeSpot.MoveDir = direction;
float scale = radius / 32f;
safeSpot.SetScale( scale );
safeSpot.Speed = Utils.Map( radius, 64f, 192f, 50f, 3f );
safeSpotGo.NetworkSpawn();
}
[Rpc.Host]
public void SpawnHealingZone( Vector2 pos, float scale )
{
var healingZoneGo = GameObject.Clone( "prefabs/healing_zone.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 150f ) ) } );
var healingZone = healingZoneGo.GetComponent<HealingZone>();
healingZone.SetScale( scale );
healingZoneGo.NetworkSpawn();
}
public void SpawnEnemyProjectile( Vector2 pos, Vector2 dir, Enemy shooter, EnemyType enemyType, float startVel, EnemyProjectileType projectileType = EnemyProjectileType.Normal, float zPos = 50f, float lifetimeModifier = 1f )
{
var prefabPath = projectileType switch
{
EnemyProjectileType.Fire => "prefabs/enemyProjectiles/spitter_projectile_fire.prefab",
EnemyProjectileType.Freeze => "prefabs/enemyProjectiles/spitter_projectile_freeze.prefab",
EnemyProjectileType.Poison => "prefabs/enemyProjectiles/spitter_projectile_poison.prefab",
EnemyProjectileType.Acid => "prefabs/enemyProjectiles/spitter_projectile_acid.prefab",
EnemyProjectileType.Curse => "prefabs/enemyProjectiles/spitter_projectile_curse.prefab",
_ => "prefabs/enemyProjectiles/spitter_projectile_normal.prefab",
};
var projectileGo = GameObject.Clone( prefabPath, new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ), Rotation.From( 0f, -Utils.GetAngleDegreesFromVector( dir ), 0f ) ) } );
var projectile = projectileGo.Components.Get<SpitterProjectile>( true );
projectile.BaseZPos = zPos;
projectile.Velocity = dir * startVel;
projectile.Shooter = shooter;
projectile.EnemyType = enemyType;
projectile.SetProjectileType( projectileType );
projectile.Lifetime *= lifetimeModifier;
projectileGo.NetworkSpawn();
}
public void SpawnEnemyHomingProjectile( Vector2 pos, Vector2 dir, Enemy shooter, EnemyType enemyType, float startVel, EnemyProjectileType projectileType = EnemyProjectileType.Normal, float zPos = 50f, float lifetimeModifier = 1f )
{
var homingPrefabPath = projectileType switch
{
EnemyProjectileType.Fire => "prefabs/enemyProjectiles/homing_spitter_projectile_fire.prefab",
EnemyProjectileType.Freeze => "prefabs/enemyProjectiles/homing_spitter_projectile_freeze.prefab",
EnemyProjectileType.Poison => "prefabs/enemyProjectiles/homing_spitter_projectile_poison.prefab",
EnemyProjectileType.Acid => "prefabs/enemyProjectiles/homing_spitter_projectile_acid.prefab",
EnemyProjectileType.Curse => "prefabs/enemyProjectiles/homing_spitter_projectile_curse.prefab",
_ => "prefabs/enemyProjectiles/homing_spitter_projectile_normal.prefab",
};
var projectileGo = GameObject.Clone( homingPrefabPath, new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ), Rotation.From( 0f, -Utils.GetAngleDegreesFromVector( dir ), 0f ) ) } );
var projectile = projectileGo.Components.Get<SpitterProjectile>( true );
projectile.BaseZPos = zPos;
projectile.Velocity = dir * startVel;
projectile.Shooter = shooter;
projectile.EnemyType = enemyType;
projectile.SetProjectileType( projectileType );
projectile.Lifetime *= lifetimeModifier;
projectileGo.NetworkSpawn();
}
public static string GetStringForEnemyType( EnemyType enemyType )
{
switch ( enemyType )
{
case EnemyType.Barrel: return "barrel";
case EnemyType.BarrelExploding: return "barrel_exploding";
case EnemyType.BarrelMystery: return "barrel_mystery";
case EnemyType.Tree: return "tree";
case EnemyType.Zombie: return "zombie";
case EnemyType.ZombieElite: return "zombie_elite";
case EnemyType.Exploder: return "exploder";
case EnemyType.ExploderElite: return "exploder_elite";
case EnemyType.Spitter: return "spitter";
case EnemyType.SpitterElite: return "spitter_elite";
case EnemyType.Spiker: return "spiker";
case EnemyType.SpikerElite: return "spiker_elite";
case EnemyType.Charger: return "charger";
case EnemyType.ChargerElite: return "charger_elite";
case EnemyType.Runner: return "runner";
case EnemyType.RunnerElite: return "runner_elite";
case EnemyType.Boss: return "boss";
case EnemyType.MinibossAbsorber: return "miniboss_absorber";
case EnemyType.MinibossCharger: return "miniboss_charger";
case EnemyType.MinibossCharger2: return "miniboss_charger_2";
case EnemyType.MinibossExploder: return "miniboss_exploder";
case EnemyType.MinibossRunner: return "miniboss_runner";
case EnemyType.MinibossShielder: return "miniboss_shielder";
case EnemyType.MinibossSlammer: return "miniboss_slammer";
case EnemyType.MinibossSniper: return "miniboss_sniper";
case EnemyType.MinibossSpiker: return "miniboss_spiker";
case EnemyType.MinibossSpiker2: return "miniboss_spiker_2";
case EnemyType.MinibossSpitter: return "miniboss_spitter";
case EnemyType.MinibossTroll: return "miniboss_troll";
case EnemyType.MinibossZapper: return "miniboss_zapper";
case EnemyType.MinibossZapper2: return "miniboss_zapper_2";
case EnemyType.MinibossZoner: return "miniboss_zoner";
case EnemyType.Snaker: return "snaker";
case EnemyType.Mushroom: return "mushroom";
case EnemyType.Chest: return "chest";
case EnemyType.ChestEvil: return "chest_evil";
case EnemyType.Turret: return "turret";
case EnemyType.BossInvincibleGenerator: return "boss_invincible_generator";
case EnemyType.ExploderMini: return "exploder_mini";
case EnemyType.ZombieTemporary: return "zombie_temporary";
}
return "";
}
public float GetThreatLevelForEnemyType( EnemyType enemyType )
{
switch ( enemyType )
{
case EnemyType.ZombieElite: return 0.5f;
case EnemyType.ExploderElite: return 1.75f;
case EnemyType.Spitter: return 1f;
case EnemyType.SpitterElite: return 1.5f;
case EnemyType.Spiker: return 1.5f;
case EnemyType.SpikerElite: return 2f;
case EnemyType.Charger: return 1.25f;
case EnemyType.ChargerElite: return 2f;
case EnemyType.Runner: return 0.66f;
case EnemyType.RunnerElite: return 2.2f;
case EnemyType.ExploderMini: return 0.9f;
}
return 0f;
}
public bool ShouldCreateSpawnClouds( EnemyType enemyType )
{
switch ( enemyType )
{
case EnemyType.Sword:
return false;
}
return true;
}
[Rpc.Broadcast]
public void SpawnSpikerHeadCracksRpc( Vector2 pos, Color color )
{
PlaySfxNearby( "spike.prepare", pos, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.85f, maxDist: 250f );
var particlesObj = GameObject.Clone( $"prefabs/effects/spiker_head_particles.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 3f ) ) } );
var particleEffect = particlesObj.GetComponent<ParticleEffect>();
particleEffect.Tint = color;
}
/// <summary>
/// For CurseSpawnSpikerHead curse.
/// </summary>
/// <param name="pos"></param>
[Rpc.Host]
public void SpawnSpikerHeadRpc( Vector2 pos)
{
var zPos = 0f;
var headGo = GameObject.Clone( $"prefabs/spiker_head.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ), new Angles( 0f, Game.Random.Float( -60f, -120f ), 0f ) ) } );
var head = headGo.Components.Get<SpikerHead>( true );
head.ModelRenderer.Tint = new Color( 0.33f, 0.33f, 0f );
headGo.NetworkSpawn();
}
public void AvoidObstacles( ref Vector2 pos, float radius, bool andSpikerHeads = false )
{
if ( andSpikerHeads )
{
foreach ( var spikerHeadArea in Scene.GetAll<SpikerHeadArea>() )
{
var distSqr = (pos - spikerHeadArea.Position2D).LengthSquared;
if ( distSqr < MathF.Pow( radius + spikerHeadArea.Radius, 2f ) )
{
pos = spikerHeadArea.Position2D + (pos - spikerHeadArea.Position2D).Normal * (radius + spikerHeadArea.Radius);
}
}
}
foreach ( var obstacle in Scene.GetAll<Obstacle>() )
{
var distSqr = (pos - obstacle.Position2D).LengthSquared;
if ( distSqr < MathF.Pow( radius + obstacle.Radius, 2f ) )
{
pos = obstacle.Position2D + (pos - obstacle.Position2D).Normal * (radius + obstacle.Radius);
}
}
}
public void SpawnSpikerHeadArea( Vector2 pos, float radius, float lifetime )
{
var spikerHeadAreaObj = GameObject.Clone( $"prefabs/spiker_head_area.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, 0f ) ) } );
var spikerHeadArea = spikerHeadAreaObj.GetComponent<SpikerHeadArea>();
spikerHeadArea.Radius = radius;
spikerHeadArea.Lifetime = lifetime;
}
public static bool IsEnemyTypeMiniboss(EnemyType enemyType)
{
switch ( enemyType )
{
case EnemyType.MinibossAbsorber:
case EnemyType.MinibossCharger:
case EnemyType.MinibossCharger2:
case EnemyType.MinibossExploder:
case EnemyType.MinibossRunner:
case EnemyType.MinibossShielder:
case EnemyType.MinibossSlammer:
case EnemyType.MinibossSniper:
case EnemyType.MinibossSpiker:
case EnemyType.MinibossSpiker2:
case EnemyType.MinibossSpitter:
case EnemyType.MinibossTroll:
case EnemyType.MinibossZapper:
case EnemyType.MinibossZapper2:
case EnemyType.MinibossZoner:
return true;
}
return false;
}
}