Manager spawning and item/effect utilities. Implements enemy types and spawn configuration, selects and spawns enemies/items/particles/floating text, handles boss/miniboss timing, weighted spawn logic and various RPC entry points for networked spawning.
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;
}
}