Replaces static Item.ItemList and related collections with thread-safe data structures using ConcurrentDictionary and ImmutableHashSet. Adds thread-safe helpers for marking items for deconstruction and managing item lists. Updates all usages of Item.ItemList and DeconstructItems to use new APIs, improving performance and safety in multi-threaded contexts. Also refactors MeleeWeapon and Projectile impact queues to use ConcurrentQueue, and updates related logic throughout the codebase.
310 lines
13 KiB
C#
310 lines
13 KiB
C#
using Barotrauma.Extensions;
|
|
using Barotrauma.Items.Components;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
partial class EndMission : Mission
|
|
{
|
|
enum MissionPhase
|
|
{
|
|
Initial,
|
|
NoItemsDestroyed,
|
|
SomeItemsDestroyed,
|
|
AllItemsDestroyed,
|
|
BossKilled
|
|
}
|
|
|
|
private readonly CharacterPrefab bossPrefab;
|
|
private readonly CharacterPrefab minionPrefab;
|
|
|
|
private readonly Identifier spawnPointTag;
|
|
private WayPoint bossSpawnPoint;
|
|
private readonly Identifier destructibleItemTag;
|
|
|
|
private readonly string endCinematicSound;
|
|
|
|
private ImmutableArray<Character> minions;
|
|
private readonly int minionCount;
|
|
private readonly float minionScatter;
|
|
|
|
private Character boss;
|
|
|
|
private readonly ItemPrefab projectilePrefab;
|
|
|
|
private float projectileTimer = 30.0f;
|
|
|
|
private readonly float startCinematicDistance = 30.0f;
|
|
|
|
private float endCinematicTimer;
|
|
|
|
private readonly List<Item> destructibleItems = new List<Item>();
|
|
|
|
protected readonly float wakeUpCinematicDelay = 5.0f;
|
|
protected readonly float bossWakeUpDelay = 7.0f;
|
|
protected readonly float cameraWaitDuration = 7.0f;
|
|
|
|
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
|
{
|
|
get { return destructibleItems.Where(it => it.Condition > 0.0f).Select(it => (Prefab.SonarLabel, it.WorldPosition)); }
|
|
}
|
|
|
|
public override int State
|
|
{
|
|
get { return base.State; }
|
|
set
|
|
{
|
|
|
|
if (state != value)
|
|
{
|
|
base.State = value;
|
|
OnStateChangedProjSpecific();
|
|
if (Phase == MissionPhase.AllItemsDestroyed)
|
|
{
|
|
CoroutineManager.Invoke(() =>
|
|
{
|
|
if (boss != null && !boss.Removed)
|
|
{
|
|
Vector2 prevPos = boss.AnimController.Collider.SimPosition;
|
|
boss.AnimController.ColliderIndex = 1;
|
|
if (bossSpawnPoint != null)
|
|
{
|
|
//ensure the new collider stays in the same position (the 2nd one has a different shape than the 1st one)
|
|
boss.AnimController.Collider.SetTransform(prevPos, 0.0f);
|
|
}
|
|
}
|
|
}, delay: wakeUpCinematicDelay + bossWakeUpDelay + 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private MissionPhase Phase
|
|
{
|
|
get
|
|
{
|
|
//state 0: nothing happens yet, play a cinematic and skip to the next state when close enough to the boss
|
|
//state 1: start cinematic played
|
|
//state 2: first destructibleItems destroyed
|
|
//state 3: 2nd destructibleItems destroyed
|
|
//state 4: all destructibleItems destroyed
|
|
//state 5: boss killed
|
|
if (state == 0) { return MissionPhase.Initial; }
|
|
if (state == 1) { return MissionPhase.NoItemsDestroyed; }
|
|
if (state < destructibleItems.Count + 1) { return MissionPhase.SomeItemsDestroyed; }
|
|
if (state < destructibleItems.Count + 2) { return MissionPhase.AllItemsDestroyed; }
|
|
return MissionPhase.BossKilled;
|
|
}
|
|
}
|
|
|
|
public EndMission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
|
: base(prefab, locations, sub)
|
|
{
|
|
Identifier speciesName = prefab.ConfigElement.GetAttributeIdentifier("bossfile", Identifier.Empty);
|
|
if (!speciesName.IsEmpty)
|
|
{
|
|
bossPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
|
if (bossPrefab == null)
|
|
{
|
|
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".",
|
|
contentPackage: Prefab.ContentPackage);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Monster file not set.",
|
|
contentPackage: Prefab.ContentPackage);
|
|
}
|
|
|
|
Identifier minionName = prefab.ConfigElement.GetAttributeIdentifier("minionfile", Identifier.Empty);
|
|
if (!minionName.IsEmpty)
|
|
{
|
|
minionPrefab = CharacterPrefab.FindBySpeciesName(minionName);
|
|
if (minionPrefab == null)
|
|
{
|
|
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".",
|
|
contentPackage: Prefab.ContentPackage);
|
|
}
|
|
}
|
|
|
|
minionCount = Math.Min(prefab.ConfigElement.GetAttributeInt(nameof(minionCount), 0), 255);
|
|
minionScatter = Math.Min(prefab.ConfigElement.GetAttributeFloat(nameof(minionScatter), 0), 10000);
|
|
|
|
Identifier projectileId = prefab.ConfigElement.GetAttributeIdentifier("projectile", Identifier.Empty);
|
|
if (!projectileId.IsEmpty)
|
|
{
|
|
projectilePrefab = MapEntityPrefab.FindByIdentifier(projectileId) as ItemPrefab;
|
|
if (projectilePrefab == null)
|
|
{
|
|
DebugConsole.ThrowError($"Error in end mission \"{prefab.Identifier}\". Could not find an item prefab with the name \"{projectileId}\".",
|
|
contentPackage: Prefab.ContentPackage);
|
|
}
|
|
}
|
|
|
|
spawnPointTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(spawnPointTag), Identifier.Empty);
|
|
destructibleItemTag = prefab.ConfigElement.GetAttributeIdentifier(nameof(destructibleItemTag), Identifier.Empty);
|
|
endCinematicSound = prefab.ConfigElement.GetAttributeString(nameof(endCinematicSound), string.Empty);
|
|
startCinematicDistance = prefab.ConfigElement.GetAttributeFloat(nameof(startCinematicDistance), 0);
|
|
}
|
|
|
|
protected override void StartMissionSpecific(Level level)
|
|
{
|
|
bossSpawnPoint = WayPoint.WayPointList.FirstOrDefault(wp => wp.Tags.Contains(spawnPointTag));
|
|
if (bossSpawnPoint == null)
|
|
{
|
|
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find a spawn point \"{spawnPointTag}\".",
|
|
contentPackage: Prefab.ContentPackage);
|
|
return;
|
|
}
|
|
if (!IsClient)
|
|
{
|
|
boss = Character.Create(bossPrefab.Identifier, bossSpawnPoint.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
|
|
var minionList = new List<Character>();
|
|
float angle = 0;
|
|
float angleStep = MathHelper.TwoPi / Math.Max(minionCount, 1);
|
|
for (int i = 0; i < minionCount; i++)
|
|
{
|
|
minionList.Add(Character.Create(minionPrefab.Identifier, MathUtils.GetPointOnCircumference(bossSpawnPoint.WorldPosition, minionScatter, angle), ToolBox.RandomSeed(8), createNetworkEvent: false));
|
|
angle += angleStep;
|
|
}
|
|
SwarmBehavior.CreateSwarm(minionList.Cast<AICharacter>());
|
|
minions = minionList.ToImmutableArray();
|
|
}
|
|
if (destructibleItemTag.IsEmpty)
|
|
{
|
|
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Destructible item tag not set.",
|
|
contentPackage: Prefab.ContentPackage);
|
|
return;
|
|
}
|
|
destructibleItems.Clear();
|
|
destructibleItems.AddRange(Item.ItemList.Where(it => it.HasTag(destructibleItemTag)));
|
|
if (destructibleItems.None())
|
|
{
|
|
DebugConsole.ThrowError($"Error in end mission \"{Prefab.Identifier}\". Could not find any destructible items with the tag \"{spawnPointTag}\".",
|
|
contentPackage: Prefab.ContentPackage);
|
|
return;
|
|
}
|
|
}
|
|
|
|
protected override void UpdateMissionSpecific(float deltaTime)
|
|
{
|
|
UpdateProjSpecific();
|
|
|
|
if (state == 0)
|
|
{
|
|
if (startCinematicDistance <= 0.0f ||
|
|
boss == null || Submarine.MainSub == null ||
|
|
Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, boss.WorldPosition) <= startCinematicDistance * startCinematicDistance)
|
|
{
|
|
State = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!IsClient && State > 0)
|
|
{
|
|
State = Math.Max(State, destructibleItems.Count(it => it.Condition <= 0.0f) + 1);
|
|
}
|
|
|
|
if (Phase == MissionPhase.AllItemsDestroyed)
|
|
{
|
|
if (projectilePrefab != null && boss != null && !boss.IsDead && !boss.Removed)
|
|
{
|
|
projectileTimer -= deltaTime;
|
|
if (projectileTimer <= 0.0f)
|
|
{
|
|
float dist = Vector2.Distance(Submarine.MainSub.WorldPosition, boss.WorldPosition);
|
|
float distanceFactor = Math.Min(dist / 10000.0f, 1.0f);
|
|
int projectileAmount = Rand.Range(3, 6);
|
|
//more concentrated shots the further the sub is
|
|
float spread = MathHelper.ToRadians(Rand.Range(20.0f, 180.0f)) * Math.Max(1.0f - distanceFactor, 0.2f);
|
|
for (int i = 0; i < projectileAmount; i++)
|
|
{
|
|
int index = i;
|
|
Entity.Spawner.AddItemToSpawnQueue(projectilePrefab, boss.WorldPosition, onSpawned: it =>
|
|
{
|
|
var projectile = it.GetComponent<Projectile>();
|
|
float angle = MathUtils.VectorToAngle(Submarine.MainSub.WorldPosition - boss.WorldPosition);
|
|
if (projectileAmount > 1)
|
|
{
|
|
angle += (index / (float)(projectileAmount - 1) - 0.5f) * spread;
|
|
}
|
|
it.body.SetTransform(it.SimPosition, angle);
|
|
it.UpdateTransform();
|
|
//faster launch velocity the further the sub is
|
|
projectile.Use(launchImpulseModifier: MathHelper.Lerp(0, 5, distanceFactor));
|
|
});
|
|
}
|
|
|
|
//the closer the sub is, more likely it is to shoot frequently
|
|
float shortIntervalProbability = MathHelper.Lerp(0.9f, 0.05f, distanceFactor);
|
|
if (Rand.Range(0.0f, 1.0f) < shortIntervalProbability)
|
|
{
|
|
projectileTimer = Rand.Range(3.0f, 5.0f);
|
|
}
|
|
else
|
|
{
|
|
projectileTimer = Rand.Range(15f, 30f);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
State = Math.Max(destructibleItems.Count + 2, State);
|
|
}
|
|
}
|
|
else if (Phase == MissionPhase.BossKilled)
|
|
{
|
|
const float EndCinematicDuration = 20.0f;
|
|
|
|
endCinematicTimer += deltaTime;
|
|
#if CLIENT
|
|
Screen.Selected.Cam.Shake = MathHelper.Clamp(MathF.Pow(endCinematicTimer, 3), 5.0f, 200.0f);
|
|
|
|
|
|
Screen.Selected.Cam.Rotation =
|
|
Math.Max((endCinematicTimer - 5.0f) * 0.05f, 0.0f)
|
|
+ (PerlinNoise.GetPerlin(endCinematicTimer * 0.1f, endCinematicTimer * 0.05f) - 0.5f) * 0.5f * (endCinematicTimer / EndCinematicDuration);
|
|
if (Rand.Range(0.0f, 100.0f) < endCinematicTimer)
|
|
{
|
|
Level.Loaded.Renderer.Flash();
|
|
}
|
|
Level.Loaded.Renderer.ChromaticAberrationStrength = endCinematicTimer * 5;
|
|
Level.Loaded.Renderer.CollapseEffectOrigin = boss.WorldPosition;
|
|
Level.Loaded.Renderer.CollapseEffectStrength = endCinematicTimer / EndCinematicDuration;
|
|
#endif
|
|
if (endCinematicTimer > 5 && !IsClient)
|
|
{
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null)
|
|
{
|
|
c.SetAllDamage(200.0f, 0.0f, 0.0f);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (endCinematicTimer > EndCinematicDuration && !IsClient)
|
|
{
|
|
//endCinematicTimer = 0;
|
|
GameMain.GameSession.Campaign?.LoadNewLevel();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
partial void UpdateProjSpecific();
|
|
|
|
partial void OnStateChangedProjSpecific();
|
|
|
|
protected override bool DetermineCompleted()
|
|
{
|
|
return Phase == MissionPhase.BossKilled;
|
|
}
|
|
}
|
|
}
|