Unstable v0.1300.0.0 (February 19th 2021)

This commit is contained in:
Joonas Rikkonen
2021-02-25 13:44:23 +02:00
parent b772654326
commit 24cbef485a
441 changed files with 21343 additions and 8562 deletions
@@ -91,6 +91,9 @@ namespace Barotrauma.MapCreatures.Behavior
public List<Tuple<Vector2, Vector2>> debugSearchLines = new List<Tuple<Vector2, Vector2>>();
#endif
private static List<BallastFloraBehavior> _entityList = new List<BallastFloraBehavior>();
public static IEnumerable<BallastFloraBehavior> EntityList => _entityList;
public enum NetworkHeader
{
Spawn,
@@ -199,13 +202,18 @@ namespace Barotrauma.MapCreatures.Behavior
[Serialize(5f, true, "How much damage is taken from open fires")]
public float FireVulnerability { get; set; }
[Serialize(0.5f, true, "How much resistance against fire is gained while submerged.")]
public float SubmergedWaterResistance { get; set; }
[Serialize(0.8f, true, "What depth the branches will be drawn on")]
public float BranchDepth { get; set; }
[Serialize("", true, "What sound to play when the ballast flora bursts thru walls")]
public string BurstSound { get; set; } = "";
private float availablePower;
private float toxinsTimer;
[Serialize(0f, true, "How much power the ballast flora has stored.")]
public float AvailablePower
{
get => availablePower;
@@ -244,7 +252,7 @@ namespace Barotrauma.MapCreatures.Behavior
public float PowerConsumptionTimer;
private float defenseCooldown, toxinsCooldown, fireCheckCooldown;
private float damageIndicatorTimer, selfDamageTimer;
private float damageIndicatorTimer, selfDamageTimer, toxinsTimer;
private readonly List<BallastFloraBranch> branchesVulnerableToFire = new List<BallastFloraBranch>();
@@ -293,6 +301,7 @@ namespace Barotrauma.MapCreatures.Behavior
LoadPrefab(prefab.Element);
StateMachine = new BallastFloraStateMachine(this);
if (firstGrowth) { GenerateStem(); }
_entityList.Add(this);
}
partial void LoadPrefab(XElement element);
@@ -373,8 +382,8 @@ namespace Barotrauma.MapCreatures.Behavior
int flowerConfig = getInt("flowerconfig");
int leafconfig = getInt("leafconfig");
int id = getInt("ID");
int health = getInt("health");
int maxhealth = getInt("maxhealth");
float health = getFloat("health");
float maxhealth = getFloat("maxhealth");
int sides = getInt("sides");
int blockedSides = getInt("blockedsides");
int claimedId = branchElement.GetAttributeInt("claimed", -1);
@@ -398,6 +407,7 @@ namespace Barotrauma.MapCreatures.Behavior
Branches.Add(newBranch);
int getInt(string name) => branchElement.GetAttributeInt(name, 0);
float getFloat(string name) => branchElement.GetAttributeFloat(name, 0f);
}
}
@@ -424,7 +434,8 @@ namespace Barotrauma.MapCreatures.Behavior
if (GameMain.DebugDraw)
{
GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, GetWorldPosition() + branch.Position, Vector2.UnitY * 10.0f, 3f, playSound: false);
var pos = (Parent?.Position ?? Vector2.Zero) + Offset + branch.Position;
GUI.AddMessage($"{(int)branch.AccumulatedDamage}", GUI.Style.Red, pos, Vector2.UnitY * 10.0f, 3f, playSound: false, subId: Parent?.Submarine?.ID ?? -1);
}
#elif SERVER
SendNetworkMessage(this, NetworkHeader.BranchDamage, branch, branch.AccumulatedDamage);
@@ -466,24 +477,8 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
}
else
{
if (selfDamageTimer <= 0)
{
if (!CanGrowMore())
{
foreach (BallastFloraBranch branch in Branches)
{
float maxHealth = branch.IsRoot ? StemHealth : BranchHealth;
DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other);
}
}
selfDamageTimer = 1f;
}
selfDamageTimer -= deltaTime;
}
UpdateSelfDamage(deltaTime);
if (Anger > 1f)
{
@@ -544,6 +539,41 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
private void UpdateSelfDamage(float deltaTime)
{
if (selfDamageTimer <= 0)
{
bool hasRoot = false;
foreach (BallastFloraBranch branch in Branches)
{
if (branch.IsRoot)
{
hasRoot = true;
break;
}
}
if (!hasRoot)
{
Kill();
return;
}
if (!HasBrokenThrough && !CanGrowMore())
{
Branches.ForEachMod(branch =>
{
float maxHealth = branch.IsRoot ? StemHealth : BranchHealth;
DamageBranch(branch, Rand.Range(1f, maxHealth), AttackType.Other);
});
}
selfDamageTimer = 1f;
}
selfDamageTimer -= deltaTime;
}
private void UpdatePowerDrain(float deltaTime)
{
PowerConsumptionTimer += deltaTime;
@@ -576,7 +606,7 @@ namespace Barotrauma.MapCreatures.Behavior
float batteryDrain = powerDelta * 0.1f;
foreach (PowerContainer battery in ClaimedBatteries)
{
float amount = Math.Max(battery.MaxOutPut, batteryDrain);
float amount = Math.Min(battery.MaxOutPut, batteryDrain);
if (battery.Charge > amount)
{
@@ -744,7 +774,7 @@ namespace Barotrauma.MapCreatures.Behavior
#if SERVER
if (!load)
{
SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true);
SendNetworkMessage(this, NetworkHeader.Infect, target.ID, true, branch);
}
#endif
}
@@ -802,7 +832,7 @@ namespace Barotrauma.MapCreatures.Behavior
Vector2 flowerPos = GetWorldPosition() + newBranch.Position;
CreateShapnel(flowerPos);
newBranch.GrowthStep = 2.0f;
SoundPlayer.PlayDamageSound("ArmorBreak", 1.0f, flowerPos, range: 800);
SoundPlayer.PlayDamageSound(BurstSound, 1.0f, flowerPos, range: 800);
}
#endif
}
@@ -840,6 +870,7 @@ namespace Barotrauma.MapCreatures.Behavior
public void DamageBranch(BallastFloraBranch branch, float amount, AttackType type, Character? attacker = null)
{
float damage = amount;
// damage is handled server side currently
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -853,7 +884,7 @@ namespace Barotrauma.MapCreatures.Behavior
{
if (IsInWater(branch))
{
return;
damage *= 1f - SubmergedWaterResistance;
}
if (defenseCooldown <= 0)
@@ -861,24 +892,24 @@ namespace Barotrauma.MapCreatures.Behavior
if (!(StateMachine.State is DefendWithPumpState))
{
StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker));
defenseCooldown = 60f;
defenseCooldown = 180f;
}
defenseCooldown = 10f;
}
}
branch.AccumulatedDamage += amount;
branch.AccumulatedDamage += damage;
branch.Health -= amount;
branch.Health -= damage;
if (type != AttackType.Other)
{
Anger += amount * 0.001f;
Anger += damage * 0.001f;
}
#if SERVER
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, amount);
GameMain.Server?.KarmaManager?.OnBallastFloraDamaged(attacker, damage);
#endif
if (branch.Health < 0)
@@ -901,6 +932,8 @@ namespace Barotrauma.MapCreatures.Behavior
{
target.Infector = null;
}
_entityList.Remove(this);
}
public void RemoveBranch(BallastFloraBranch branch)
@@ -935,18 +968,10 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
}
});
});
#if CLIENT
Vector2 pos = GetWorldPosition() + branch.Position;
GameMain.ParticleManager.CreateParticle("bloodsplash", pos, Rand.Range(0, 360), Rand.Range(0, 100));
GameMain.ParticleManager.CreateParticle("waterblood", pos, Rand.Range(0, 360), 0);
for (int i = 0; i < 4; i++)
{
GameMain.ParticleManager.CreateParticle("gib", pos, Rand.Range(0, 360), Rand.Range(100f, 300f));
}
CreateDeathParticle(branch);
#endif
if (isClient) { return; }
@@ -1014,6 +1039,8 @@ namespace Barotrauma.MapCreatures.Behavior
target.Infector = null;
}
StateMachine?.State?.Exit();
// clean up leftover (can probably be removed)
foreach (Body body in bodies)
{
@@ -1041,7 +1068,7 @@ namespace Barotrauma.MapCreatures.Behavior
CreateShapnel(GetWorldPosition() + branch.Position);
}
SoundPlayer.PlayDamageSound("ArmorBreak", BreakthroughPoint, GetWorldPosition(), range: 800);
SoundPlayer.PlayDamageSound(BurstSound, BreakthroughPoint, GetWorldPosition(), range: 800);
#endif
}
@@ -51,6 +51,7 @@ namespace Barotrauma.MapCreatures.Behavior
if (pump.Item.CurrentHull == targetBranch.CurrentHull)
{
targetPumps.Add(pump);
SetPump(pump);
pump.Hijacked = true;
}
}
@@ -90,18 +91,23 @@ namespace Barotrauma.MapCreatures.Behavior
}
}
private void SetPump(Pump pump)
{
if (pump.TargetLevel != null)
{
pump.TargetLevel = 100f;
}
else
{
pump.FlowPercentage = 100f;
}
}
public void Update(float deltaTime)
{
foreach (Pump pump in targetPumps)
{
if (pump.TargetLevel != null)
{
pump.TargetLevel = 100f;
}
else
{
pump.FlowPercentage = 100f;
}
SetPump(pump);
}
if (tryDrown && !filled)
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
@@ -25,6 +26,17 @@ namespace Barotrauma.MapCreatures.Behavior
protected override void Grow()
{
if (TargetBranches.Any(b => b.Removed))
{
if (!Behavior.IgnoredTargets.ContainsKey(Target))
{
Behavior.IgnoredTargets.Add(Target, 10);
}
isFinished = true;
return;
}
if (Target == null || Target.Removed)
{
isFinished = true;
@@ -12,8 +12,9 @@ namespace Barotrauma
public const ushort NullEntityID = 0;
public const ushort EntitySpawnerID = ushort.MaxValue;
public const ushort RespawnManagerID = ushort.MaxValue - 1;
public const ushort DummyID = ushort.MaxValue - 2;
public const ushort ReservedIDStart = ushort.MaxValue - 2;
public const ushort ReservedIDStart = ushort.MaxValue - 3;
private static Dictionary<ushort, Entity> dictionary = new Dictionary<ushort, Entity>();
public static IEnumerable<Entity> GetEntities()
@@ -15,7 +15,7 @@ namespace Barotrauma
{
private static readonly List<Triplet<Explosion, Vector2, float>> prevExplosions = new List<Triplet<Explosion, Vector2, float>>();
private readonly Attack attack;
public readonly Attack Attack;
private readonly float force;
@@ -25,7 +25,11 @@ namespace Barotrauma
private readonly float screenColorRange, screenColorDuration;
private bool sparks, shockwave, flames, smoke, flash, underwaterBubble;
private bool playTinnitus;
private bool applyFireEffects;
private bool ignoreCover;
private bool onlyInside;
private bool onlyOutside;
private readonly float flashDuration;
private readonly float? flashRange;
private readonly string decal;
@@ -35,14 +39,15 @@ namespace Barotrauma
public float BallastFloraDamage { get; set; }
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f)
public Explosion(float range, float force, float damage, float structureDamage, float itemDamage, float empStrength = 0.0f, float ballastFloraStrength = 0.0f)
{
attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range)
Attack = new Attack(damage, 0.0f, 0.0f, structureDamage, itemDamage, range)
{
SeverLimbsProbability = 1.0f
};
this.force = force;
this.EmpStrength = empStrength;
BallastFloraDamage = ballastFloraStrength;
sparks = true;
shockwave = true;
smoke = true;
@@ -52,7 +57,7 @@ namespace Barotrauma
public Explosion(XElement element, string parentDebugName)
{
attack = new Attack(element, parentDebugName + ", Explosion");
Attack = new Attack(element, parentDebugName + ", Explosion");
force = element.GetAttributeFloat("force", 0.0f);
@@ -62,7 +67,12 @@ namespace Barotrauma
underwaterBubble = element.GetAttributeBool("underwaterbubble", true);
smoke = element.GetAttributeBool("smoke", true);
playTinnitus = element.GetAttributeBool("playtinnitus", true);
applyFireEffects = element.GetAttributeBool("applyfireeffects", flames);
ignoreCover = element.GetAttributeBool("ignorecover", false);
onlyInside = element.GetAttributeBool("onlyinside", false);
onlyOutside = element.GetAttributeBool("onlyoutside", false);
flash = element.GetAttributeBool("flash", true);
flashDuration = element.GetAttributeFloat("flashduration", 0.05f);
@@ -74,10 +84,10 @@ namespace Barotrauma
decal = element.GetAttributeString("decal", "");
decalSize = element.GetAttributeFloat(1.0f, "decalSize", "decalsize");
cameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f);
cameraShakeRange = element.GetAttributeFloat("camerashakerange", attack.Range);
cameraShake = element.GetAttributeFloat("camerashake", Attack.Range * 0.1f);
cameraShakeRange = element.GetAttributeFloat("camerashakerange", Attack.Range);
screenColorRange = element.GetAttributeFloat("screencolorrange", attack.Range * 0.1f);
screenColorRange = element.GetAttributeFloat("screencolorrange", Attack.Range * 0.1f);
screenColor = element.GetAttributeColor("screencolor", Color.Transparent);
screenColorDuration = element.GetAttributeFloat("screencolorduration", 0.1f);
}
@@ -113,7 +123,7 @@ namespace Barotrauma
hull.AddDecal(decal, worldPosition, decalSize, isNetworkEvent: false);
}
float displayRange = attack.Range;
float displayRange = Attack.Range;
Vector2 cameraPos = Character.Controlled != null ? Character.Controlled.WorldPosition : GameMain.GameScreen.Cam.Position;
float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f;
@@ -128,9 +138,9 @@ namespace Barotrauma
if (displayRange < 0.1f) { return; }
if (attack.GetStructureDamage(1.0f) > 0.0f)
if (Attack.GetStructureDamage(1.0f) > 0.0f)
{
RangedStructureDamage(worldPosition, displayRange, attack.GetStructureDamage(1.0f), attacker);
RangedStructureDamage(worldPosition, displayRange, Attack.GetStructureDamage(1.0f), Attack.GetLevelWallDamage(1.0f), attacker);
}
if (BallastFloraDamage > 0.0f)
@@ -165,12 +175,12 @@ namespace Barotrauma
}
}
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(attack.Stun, 0.0f) && MathUtils.NearlyEqual(attack.GetTotalDamage(false), 0.0f))
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && MathUtils.NearlyEqual(Attack.GetTotalDamage(false), 0.0f))
{
return;
}
DamageCharacters(worldPosition, attack, force, damageSource, attacker);
DamageCharacters(worldPosition, Attack, force, damageSource, attacker);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -180,9 +190,9 @@ namespace Barotrauma
float dist = Vector2.Distance(item.WorldPosition, worldPosition);
float itemRadius = item.body == null ? 0.0f : item.body.GetMaxExtent();
dist = Math.Max(0.0f, dist - ConvertUnits.ToDisplayUnits(itemRadius));
if (dist > attack.Range) { continue; }
if (dist > Attack.Range) { continue; }
if (dist < attack.Range * 0.5f && applyFireEffects && !item.FireProof)
if (dist < Attack.Range * 0.5f && applyFireEffects && !item.FireProof)
{
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
@@ -209,8 +219,8 @@ namespace Barotrauma
if (item.Prefab.DamagedByExplosions && !item.Indestructible)
{
float distFactor = 1.0f - dist / attack.Range;
float damageAmount = attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier;
float distFactor = 1.0f - dist / Attack.Range;
float damageAmount = Attack.GetItemDamage(1.0f) * item.Prefab.ExplosionDamageMultiplier;
Vector2 explosionPos = worldPosition;
if (item.Submarine != null) { explosionPos -= item.Submarine.Position; }
@@ -224,7 +234,7 @@ namespace Barotrauma
partial void ExplodeProjSpecific(Vector2 worldPosition, Hull hull);
public static void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
private void DamageCharacters(Vector2 worldPosition, Attack attack, float force, Entity damageSource, Character attacker)
{
if (attack.Range <= 0.0f) { return; }
@@ -239,6 +249,8 @@ namespace Barotrauma
{
continue;
}
if (onlyInside && c.Submarine == null) { continue; }
else if (onlyOutside && c.Submarine != null) { continue; }
Vector2 explosionPos = worldPosition;
if (c.Submarine != null) { explosionPos -= c.Submarine.Position; }
@@ -253,7 +265,7 @@ namespace Barotrauma
List<Affliction> modifiedAfflictions = new List<Affliction>();
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered || limb.IgnoreCollisions) { continue; }
if (limb.IsSevered || limb.IgnoreCollisions || !limb.body.Enabled) { continue; }
float dist = Vector2.Distance(limb.WorldPosition, worldPosition);
@@ -267,7 +279,10 @@ namespace Barotrauma
float distFactor = 1.0f - dist / attack.Range;
//solid obstacles between the explosion and the limb reduce the effect of the explosion
distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition);
if (!ignoreCover)
{
distFactor *= GetObstacleDamageMultiplier(explosionPos, worldPosition, limb.SimPosition);
}
distFactors.Add(limb, distFactor);
modifiedAfflictions.Clear();
@@ -322,10 +337,10 @@ namespace Barotrauma
}
}
if (c == Character.Controlled && !c.IsDead)
if (c == Character.Controlled && !c.IsDead && playTinnitus)
{
Limb head = c.AnimController.GetLimb(LimbType.Head);
if (damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor))
if (head != null && damages.TryGetValue(head, out float headDamage) && headDamage > 0.0f && distFactors.TryGetValue(head, out float headFactor))
{
PlayTinnitusProjSpecific(headFactor);
}
@@ -354,7 +369,7 @@ namespace Barotrauma
/// <summary>
/// Returns a dictionary where the keys are the structures that took damage and the values are the amount of damage taken
/// </summary>
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, Character attacker = null, bool damageLevelWalls = true)
public static Dictionary<Structure, float> RangedStructureDamage(Vector2 worldPosition, float worldRange, float damage, float levelWallDamage, Character attacker = null)
{
List<Structure> structureList = new List<Structure>();
float dist = 600.0f;
@@ -391,7 +406,7 @@ namespace Barotrauma
}
}
if (Level.Loaded != null && damageLevelWalls)
if (Level.Loaded != null && !MathUtils.NearlyEqual(levelWallDamage, 0.0f))
{
for (int i = Level.Loaded.ExtraWalls.Count - 1; i >= 0; i--)
{
@@ -400,7 +415,7 @@ namespace Barotrauma
{
if (cell.IsPointInside(worldPosition))
{
destructibleWall.AddDamage(damage, worldPosition);
destructibleWall.AddDamage(levelWallDamage, worldPosition);
continue;
}
foreach (var edge in cell.Edges)
@@ -299,7 +299,7 @@ namespace Barotrauma
//GetApproximateDistance returns float.MaxValue if there's no path through open gaps between the hulls (e.g. if there's a door/wall in between)
if (hull.GetApproximateDistance(Position, c.Position, c.CurrentHull, 10000.0f) > size.X + DamageRange)
{
return;
continue;
}
float dmg = (float)Math.Sqrt(Math.Min(500, size.X)) * deltaTime / c.AnimController.Limbs.Count(l => !l.IsSevered && !l.Hidden);
@@ -346,11 +346,17 @@ namespace Barotrauma
//don't apply OnFire effects if the item is inside a fireproof container
//(or if it's inside a container that's inside a fireproof container, etc)
Item container = item.Container;
bool fireProof = false;
while (container != null)
{
if (container.FireProof) return;
if (container.FireProof)
{
fireProof = true;
break;
}
container = container.Container;
}
if (fireProof) { continue; }
float range = (float)Math.Sqrt(size.X) * 10.0f;
if (item.Position.X < position.X - range || item.Position.X > position.X + size.X + range) { continue; }
@@ -1,11 +1,9 @@
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Xml.Linq;
namespace Barotrauma
@@ -120,10 +118,17 @@ namespace Barotrauma
return "Gap";
}
}
public Gap(MapEntityPrefab prefab, Rectangle rectangle)
: this (rectangle, Submarine.MainSub)
{ }
: this(rectangle, Submarine.MainSub)
{
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
}
#endif
}
public Gap(Rectangle rect, Submarine submarine)
: this(rect, rect.Width < rect.Height, submarine)
@@ -233,6 +238,13 @@ namespace Barotrauma
{
Hull[] hulls = new Hull[2];
foreach (var linked in linkedTo)
{
if (linked is Hull hull)
{
hull.ConnectedGaps.Remove(this);
}
}
linkedTo.Clear();
Vector2[] searchPos = new Vector2[2];
@@ -595,7 +607,7 @@ namespace Barotrauma
}
Vector2 rayStart = ConvertUnits.ToSimUnits(WorldPosition);
Vector2 rayEnd = rayStart + rayDir * 500.0f;
Vector2 rayEnd = rayStart + rayDir * 5.0f;
var levelCells = Level.Loaded.GetCells(WorldPosition, searchDepth: 1);
foreach (var cell in levelCells)
@@ -397,7 +397,12 @@ namespace Barotrauma
public Hull(MapEntityPrefab prefab, Rectangle rectangle)
: this (prefab, rectangle, Submarine.MainSub)
{
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
}
#endif
}
public Hull(MapEntityPrefab prefab, Rectangle rectangle, Submarine submarine, ushort id = Entity.NullEntityID)
@@ -791,7 +796,7 @@ namespace Barotrauma
//make waves propagate through horizontal gaps
foreach (Gap gap in ConnectedGaps)
{
if (this != gap.linkedTo[0] as Hull)
if (this != gap.linkedTo.FirstOrDefault() as Hull)
{
//let the first linked hull handle the water propagation
continue;
@@ -934,14 +939,14 @@ namespace Barotrauma
/// Approximate distance from this hull to the target hull, moving through open gaps without passing through walls.
/// Uses a greedy algo and may not use the most optimal path. Returns float.MaxValue if no path is found.
/// </summary>
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance)
public float GetApproximateDistance(Vector2 startPos, Vector2 endPos, Hull targetHull, float maxDistance, float distanceMultiplierPerClosedDoor = 0)
{
return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance);
return GetApproximateHullDistance(startPos, endPos, new HashSet<Hull>(), targetHull, 0.0f, maxDistance, distanceMultiplierPerClosedDoor);
}
private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance)
private float GetApproximateHullDistance(Vector2 startPos, Vector2 endPos, HashSet<Hull> connectedHulls, Hull target, float distance, float maxDistance, float distanceMultiplierFromDoors = 0)
{
if (distance >= maxDistance) return float.MaxValue;
if (distance >= maxDistance) { return float.MaxValue; }
if (this == target)
{
return distance + Vector2.Distance(startPos, endPos);
@@ -951,12 +956,17 @@ namespace Barotrauma
foreach (Gap g in ConnectedGaps)
{
float distanceMultiplier = 1;
if (g.ConnectedDoor != null && !g.ConnectedDoor.IsBroken)
{
//gap blocked if the door is not open or the predicted state is not open
if ((!g.ConnectedDoor.IsOpen && !g.ConnectedDoor.IsBroken) || (g.ConnectedDoor.PredictedState.HasValue && !g.ConnectedDoor.PredictedState.Value))
{
if (g.ConnectedDoor.OpenState < 0.1f) continue;
if (g.ConnectedDoor.OpenState < 0.1f)
{
if (distanceMultiplierFromDoors <= 0) { continue; }
distanceMultiplier *= distanceMultiplierFromDoors;
}
}
}
else if (g.Open <= 0.0f)
@@ -968,8 +978,11 @@ namespace Barotrauma
{
if (g.linkedTo[i] is Hull hull && !connectedHulls.Contains(hull))
{
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position), maxDistance);
if (dist < float.MaxValue) { return dist; }
float dist = hull.GetApproximateHullDistance(g.Position, endPos, connectedHulls, target, distance + Vector2.Distance(startPos, g.Position) * distanceMultiplier, maxDistance);
if (dist < float.MaxValue)
{
return dist;
}
}
}
}
@@ -8,6 +8,11 @@ namespace Barotrauma
Vector2 WorldPosition { get; }
Vector2 SimPosition { get; }
Submarine Submarine { get; }
bool IgnoreByAI => false;
}
interface IIgnorable : ISpatialEntity
{
bool IgnoreByAI { get; }
bool OrderedToBeIgnored { get; set; }
}
}
@@ -65,8 +65,21 @@ namespace Barotrauma
var containerElement = entityElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("itemcontainer", StringComparison.OrdinalIgnoreCase));
if (containerElement == null) { continue; }
var itemIds = containerElement.GetAttributeIntArray("contained", new int[0]);
containedItemIDs.AddRange(itemIds.Select(id => (ushort)id));
string containedString = containerElement.GetAttributeString("contained", "");
string[] itemIdStrings = containedString.Split(',');
var itemIds = new List<ushort>[itemIdStrings.Length];
for (int i = 0; i < itemIdStrings.Length; i++)
{
itemIds[i] ??= new List<ushort>();
foreach (string idStr in itemIdStrings[i].Split(';'))
{
if (int.TryParse(idStr, out int id))
{
itemIds[i].Add((ushort)id);
containedItemIDs.Add((ushort)id);
}
}
}
}
int minX = int.MaxValue, minY = int.MaxValue;
@@ -110,23 +123,30 @@ namespace Barotrauma
protected override void CreateInstance(Rectangle rect)
{
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
#if CLIENT
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub, selectInstance: Screen.Selected == GameMain.SubEditorScreen);
if (Screen.Selected is SubEditorScreen)
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(loaded, false, handleInventoryBehavior: false));
}
#else
var loaded = CreateInstance(rect.Location.ToVector2(), Submarine.MainSub);
#endif
}
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectPrefabs = false)
public List<MapEntity> CreateInstance(Vector2 position, Submarine sub, bool selectInstance = false)
{
return PasteEntities(position, sub, configElement, FilePath, selectInstance);
}
public static List<MapEntity> PasteEntities(Vector2 position, Submarine sub, XElement configElement, string filePath = null, bool selectInstance = false)
{
int idOffset = Entity.FindFreeID(1);
if (MapEntity.mapEntityList.Any()) { idOffset = MapEntity.mapEntityList.Max(e => e.ID); }
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, FilePath, idOffset);
List<MapEntity> entities = MapEntity.LoadAll(sub, configElement, filePath, idOffset);
if (entities.Count == 0) { return entities; }
Vector2 offset = sub == null ? Vector2.Zero : sub.HiddenSubPosition;
Vector2 offset = sub?.HiddenSubPosition ?? Vector2.Zero;
foreach (MapEntity me in entities)
{
@@ -148,14 +168,13 @@ namespace Barotrauma
MapEntity.MapLoaded(entities, true);
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen && selectPrefabs)
if (Screen.Selected == GameMain.SubEditorScreen && selectInstance)
{
MapEntity.SelectedList.Clear();
entities.ForEach(MapEntity.AddSelection);
}
#endif
#endif
return entities;
}
public void Delete()
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
@@ -100,19 +101,19 @@ namespace Barotrauma
public Sprite WallSprite { get; private set; }
public Sprite WallEdgeSprite { get; private set; }
public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, Rand.RandSync rand)
public static CaveGenerationParams GetRandom(LevelGenerationParams generationParams, bool abyss, Rand.RandSync rand)
{
if (CaveParams.All(p => p.GetCommonness(generationParams) <= 0.0f))
if (CaveParams.All(p => p.GetCommonness(generationParams, abyss) <= 0.0f))
{
return CaveParams.First();
}
return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams)).ToList(), rand);
return ToolBox.SelectWeightedRandom(CaveParams, CaveParams.Select(p => p.GetCommonness(generationParams, abyss)).ToList(), rand);
}
public float GetCommonness(LevelGenerationParams generationParams)
public float GetCommonness(LevelGenerationParams generationParams, bool abyss)
{
if (generationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness))
OverrideCommonness.TryGetValue(abyss ? "abyss" : generationParams.Identifier, out float commonness))
{
return commonness;
}
@@ -135,6 +136,13 @@ namespace Barotrauma
case "walledge":
WallEdgeSprite = new Sprite(subElement);
break;
case "overridecommonness":
string levelType = subElement.GetAttributeString("leveltype", "").ToLowerInvariant();
if (!OverrideCommonness.ContainsKey(levelType))
{
OverrideCommonness.Add(levelType, subElement.GetAttributeFloat("commonness", 1.0f));
}
break;
}
}
}
@@ -193,5 +201,30 @@ namespace Barotrauma
}
}
}
public void Save(XElement element)
{
SerializableProperty.SerializeProperties(this, element, true);
foreach (KeyValuePair<string, float> overrideCommonness in OverrideCommonness)
{
bool elementFound = false;
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("overridecommonness", StringComparison.OrdinalIgnoreCase)
&& subElement.GetAttributeString("leveltype", "").Equals(overrideCommonness.Key, StringComparison.OrdinalIgnoreCase))
{
subElement.Attribute("commonness").Value = overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture);
elementFound = true;
break;
}
}
if (!elementFound)
{
element.Add(new XElement("overridecommonness",
new XAttribute("leveltype", overrideCommonness.Key),
new XAttribute("commonness", overrideCommonness.Value.ToString("G", CultureInfo.InvariantCulture))));
}
}
}
}
}
@@ -141,69 +141,36 @@ namespace Barotrauma
return cells;
}
private static Vector2 GetEdgeNormal(GraphEdge edge, VoronoiCell cell = null)
{
if (cell == null) { cell = edge.AdjacentCell(null); }
if (cell == null) { return Vector2.UnitX; }
CompareCCW compare = new CompareCCW(cell.Center);
if (compare.Compare(edge.Point1, edge.Point2) == -1)
{
var temp = edge.Point1;
edge.Point1 = edge.Point2;
edge.Point2 = temp;
}
Vector2 normal = Vector2.Normalize(edge.Point2 - edge.Point1);
Vector2 diffToCell = Vector2.Normalize(cell.Center - edge.Point2);
normal = new Vector2(-normal.Y, normal.X);
if (Vector2.Dot(normal, diffToCell) < 0)
{
normal = -normal;
}
return normal;
}
public static void GeneratePath(Level.Tunnel tunnel, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, Rectangle limits)
public static void GeneratePath(Level.Tunnel tunnel, Level level)
{
var targetCells = new List<VoronoiCell>();
for (int i = 0; i < tunnel.Nodes.Count; i++)
{
//a search depth of 2 is large enough to find a cell in almost all maps, but in case it fails, we increase the depth
int searchDepth = 2;
while (searchDepth < 5)
var closestCell = level.GetClosestCell(tunnel.Nodes[i].ToVector2());
if (closestCell != null && !targetCells.Contains(closestCell))
{
int cellIndex = FindCellIndex(tunnel.Nodes[i], cells, cellGrid, gridCellSize, searchDepth);
if (cellIndex > -1)
{
targetCells.Add(cells[cellIndex]);
break;
}
searchDepth++;
targetCells.Add(closestCell);
}
}
tunnel.Cells.AddRange(GeneratePath(targetCells, cells, limits));
tunnel.Cells.AddRange(GeneratePath(targetCells, level.GetAllCells()));
}
public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells, Rectangle limits)
public static List<VoronoiCell> GeneratePath(List<VoronoiCell> targetCells, List<VoronoiCell> cells)
{
Stopwatch sw2 = new Stopwatch();
sw2.Start();
List<VoronoiCell> pathCells = new List<VoronoiCell>();
if (targetCells.Count == 0) { return pathCells; }
VoronoiCell currentCell = targetCells[0];
currentCell.CellType = CellType.Path;
pathCells.Add(currentCell);
int currentTargetIndex = 0;
int iterationsLeft = cells.Count;
int iterationsLeft = cells.Count / 2;
do
{
@@ -216,10 +183,14 @@ namespace Barotrauma
if (adjacentCell == null) { continue; }
double dist = MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, targetCells[currentTargetIndex].Site.Coord.X, targetCells[currentTargetIndex].Site.Coord.Y);
dist += MathUtils.Distance(adjacentCell.Site.Coord.X, adjacentCell.Site.Coord.Y, currentCell.Site.Coord.X, currentCell.Site.Coord.Y) * 0.5f;
//disfavor small edges to prevent generating a very small passage
if (Vector2.Distance(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 200.0f)
//disfavor short edges to prevent generating a very small passage
if (Vector2.DistanceSquared(currentCell.Edges[i].Point1, currentCell.Edges[i].Point2) < 150.0f * 150.0f)
{
dist += 1000000;
//divide by the number of times the current cell has been used
// prevents the path from getting "stuck" (jumping back and forth between adjacent cells)
// if there's no other way to the destination than going through a short edge
dist *= 10.0f / Math.Max(pathCells.Count(c => c == currentCell), 1.0f);
}
if (dist < smallestDist)
{
@@ -258,7 +229,7 @@ namespace Barotrauma
List<GraphEdge> tempEdges = new List<GraphEdge>();
foreach (GraphEdge edge in cell.Edges)
{
if (!edge.IsSolid)
if (!edge.IsSolid || edge.OutsideLevel)
{
tempEdges.Add(edge);
continue;
@@ -289,7 +260,8 @@ namespace Barotrauma
}
List<Vector2> edgePoints = new List<Vector2>();
Vector2 edgeNormal = GetEdgeNormal(edge, cell);
Vector2 edgeNormal = edge.GetNormal(cell);
float edgeLength = Vector2.Distance(edge.Point1, edge.Point2);
int pointCount = (int)Math.Max(Math.Ceiling(edgeLength / minEdgeLength), 1);
Vector2 edgeDir = edge.Point2 - edge.Point1;
@@ -310,12 +282,39 @@ namespace Barotrauma
float randomVariance = Rand.Range(0, irregularity, Rand.RandSync.Server);
Vector2 extrudedPoint =
edge.Point1 +
edgeDir * (i / (float)pointCount) -
edgeDir * (i / (float)pointCount) +
edgeNormal * edgeLength * (roundingAmount + randomVariance) * centerF;
//check if extruding the edge causes it to go inside another one
var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 1);
if (!nearbyCells.Any(c => c.CellType == CellType.Solid && c != cell && c.IsPointInside(extrudedPoint))) { edgePoints.Add(extrudedPoint); }
var nearbyCells = Level.Loaded.GetCells(extrudedPoint, searchDepth: 2);
bool isInside = false;
foreach (var nearbyCell in nearbyCells)
{
if (nearbyCell == cell || nearbyCell.CellType != CellType.Solid) { continue; }
//check if extruding the edge causes it to go inside another one
if (nearbyCell.IsPointInside(extrudedPoint))
{
isInside = true;
break;
}
//check if another edge will be inside this cell after the extrusion
Vector2 triangleCenter = (edge.Point1 + edge.Point2 + extrudedPoint) / 3;
foreach (GraphEdge nearbyEdge in nearbyCell.Edges)
{
if (!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) &&
!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) &&
!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2))
{
isInside = true;
break;
}
}
if (isInside) { break; }
}
if (!isInside)
{
edgePoints.Add(extrudedPoint);
}
}
}
@@ -381,7 +380,19 @@ namespace Barotrauma
continue;
}
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, cell.Center));
Vector2 minVert = tempVertices[0];
Vector2 maxVert = tempVertices[0];
foreach (var vert in tempVertices)
{
minVert = new Vector2(
Math.Min(minVert.X, vert.X),
Math.Min(minVert.Y, vert.Y));
maxVert = new Vector2(
Math.Max(maxVert.X, vert.X),
Math.Max(maxVert.Y, vert.Y));
}
Vector2 center = (minVert + maxVert) / 2;
renderTriangles.AddRange(MathUtils.TriangulateConvexHull(tempVertices, center));
if (bodyPoints.Count < 2) { continue; }
@@ -404,7 +415,7 @@ namespace Barotrauma
if (cell.CellType == CellType.Empty) { continue; }
cellBody.UserData = cell;
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(cell.Center));
var triangles = MathUtils.TriangulateConvexHull(bodyPoints, ConvertUnits.ToSimUnits(center));
for (int i = 0; i < triangles.Count; i++)
{
@@ -435,14 +446,21 @@ namespace Barotrauma
}
cell.Body = cellBody;
}
cellBody.CollisionCategories = Physics.CollisionLevel;
cellBody.ResetMassData();
return cellBody;
}
public static List<Vector2> CreateRandomChunk(float radius, int vertexCount, float radiusVariance)
{
Debug.Assert(radiusVariance < radius);
return CreateRandomChunk(radius * 2, radius * 2, vertexCount, radiusVariance);
}
public static List<Vector2> CreateRandomChunk(float width, float height, int vertexCount, float radiusVariance)
{
Debug.Assert(radiusVariance < Math.Min(width, height));
Debug.Assert(vertexCount >= 3);
List<Vector2> verts = new List<Vector2>();
@@ -450,72 +468,12 @@ namespace Barotrauma
float angle = 0.0f;
for (int i = 0; i < vertexCount; i++)
{
verts.Add(new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) *
(radius + Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server)));
Vector2 dir = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
verts.Add(new Vector2(dir.X * width / 2, dir.Y * height / 2) + dir * Rand.Range(-radiusVariance, radiusVariance, Rand.RandSync.Server));
angle += angleStep;
}
return verts;
}
/// <summary>
/// find the index of the cell which the point is inside
/// (actually finds the cell whose center is closest, but it's always the correct cell assuming the point is inside the borders of the diagram)
/// </summary>
public static int FindCellIndex(Vector2 position,List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1, Vector2? offset = null)
{
float closestDist = float.PositiveInfinity;
VoronoiCell closestCell = null;
Vector2 gridOffset = offset == null ? Vector2.Zero : (Vector2)offset;
position -= gridOffset;
int gridPosX = (int)Math.Floor(position.X / gridCellSize);
int gridPosY = (int)Math.Floor(position.Y / gridCellSize);
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
{
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
{
for (int i = 0; i < cellGrid[x, y].Count; i++)
{
float dist = Vector2.DistanceSquared(cellGrid[x, y][i].Center, position);
if (dist > closestDist) continue;
closestDist = dist;
closestCell = cellGrid[x, y][i];
}
}
}
return cells.IndexOf(closestCell);
}
public static int FindCellIndex(Point position, List<VoronoiCell> cells, List<VoronoiCell>[,] cellGrid, int gridCellSize, int searchDepth = 1)
{
int closestDist = int.MaxValue;
VoronoiCell closestCell = null;
int gridPosX = position.X / gridCellSize;
int gridPosY = position.Y / gridCellSize;
for (int x = Math.Max(gridPosX - searchDepth, 0); x <= Math.Min(gridPosX + searchDepth, cellGrid.GetLength(0) - 1); x++)
{
for (int y = Math.Max(gridPosY - searchDepth, 0); y <= Math.Min(gridPosY + searchDepth, cellGrid.GetLength(1) - 1); y++)
{
for (int i = 0; i < cellGrid[x, y].Count; i++)
{
int dist = MathUtils.DistanceSquared(
(int)cellGrid[x, y][i].Site.Coord.X, (int)cellGrid[x, y][i].Site.Coord.Y,
position.X, position.Y);
if (dist > closestDist) continue;
closestDist = dist;
closestCell = cellGrid[x, y][i];
}
}
}
return cells.IndexOf(closestCell);
}
}
}
@@ -58,7 +58,8 @@ namespace Barotrauma
public DestructibleLevelWall(List<Vector2> vertices, Color color, Level level, float? health = null, bool giftWrap = false)
: base (vertices, color, level, giftWrap)
{
MaxHealth = health ?? MathHelper.Clamp(Body.Mass, 100.0f, 1000.0f);
MaxHealth = health ?? MathHelper.Clamp(Body.Mass * 0.5f, 50.0f, 1000.0f);
Cells.ForEach(c => c.IsDestructible = true);
}
public override void Update(float deltaTime)
@@ -201,6 +202,10 @@ namespace Barotrauma
if (Destroyed) { return; }
Destroyed = true;
level?.UnsyncedExtraWalls?.Remove(this);
foreach (var cell in Cells)
{
cell.CellType = CellType.Removed;
}
GameMain.World.Remove(Body);
Dispose();
}
File diff suppressed because it is too large Load Diff
@@ -29,12 +29,19 @@ namespace Barotrauma
public bool HasBeaconStation;
public bool IsBeaconActive;
public bool HasHuntingGrounds;
public OutpostGenerationParams ForceOutpostGenerationParams;
public readonly Point Size;
public readonly int InitialDepth;
/// <summary>
/// Determined during level generation based on the size of the submarine. Null if the level hasn't been generated.
/// </summary>
public int? MinMainPathWidth;
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
@@ -81,6 +88,8 @@ namespace Barotrauma
HasBeaconStation = element.GetAttributeBool("hasbeaconstation", false);
IsBeaconActive = element.GetAttributeBool("isbeaconactive", false);
HasHuntingGrounds = element.GetAttributeBool("hashuntinggrounds", false);
string generationParamsId = element.GetAttributeString("generationparams", "");
GenerationParams = LevelGenerationParams.LevelParams.Find(l => l.Identifier == generationParamsId || l.OldIdentifier == generationParamsId);
if (GenerationParams == null)
@@ -96,7 +105,7 @@ namespace Barotrauma
InitialDepth = element.GetAttributeInt("initialdepth", GenerationParams.InitialDepthMin);
string biomeIdentifier = element.GetAttributeString("biome", "");
Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier);
Biome = LevelGenerationParams.GetBiomes().FirstOrDefault(b => b.Identifier == biomeIdentifier || b.OldIdentifier == biomeIdentifier);
if (Biome == null)
{
DebugConsole.ThrowError($"Error in level data: could not find the biome \"{biomeIdentifier}\".");
@@ -135,7 +144,10 @@ namespace Barotrauma
var rand = new MTRandom(ToolBox.StringToInt(Seed));
InitialDepth = (int)MathHelper.Lerp(GenerationParams.InitialDepthMin, GenerationParams.InitialDepthMax, (float)rand.NextDouble());
HasBeaconStation = rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
float maxHuntingGroundsProbability = 0.3f;
HasHuntingGrounds = rand.NextDouble() < Difficulty / 100.0f * maxHuntingGroundsProbability;
HasBeaconStation = !HasHuntingGrounds && rand.NextDouble() < locationConnection.Locations.Select(l => l.Type.BeaconStationChance).Max();
IsBeaconActive = false;
}
@@ -158,7 +170,7 @@ namespace Barotrauma
(int)MathUtils.Round(GenerationParams.Height, Level.GridCellSize));
}
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null)
public static LevelData CreateRandom(string seed = "", float? difficulty = null, LevelGenerationParams generationParams = null, bool requireOutpost = false)
{
if (string.IsNullOrEmpty(seed))
{
@@ -167,25 +179,34 @@ namespace Barotrauma
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
LevelType type = generationParams == null ? LevelData.LevelType.LocationConnection : generationParams.Type;
LevelType type = generationParams == null ?
(requireOutpost ? LevelType.Outpost : LevelType.LocationConnection) :
generationParams.Type;
if (generationParams == null) { generationParams = LevelGenerationParams.GetRandom(seed, type); }
var biome =
LevelGenerationParams.GetBiomes().FirstOrDefault(b => generationParams.AllowedBiomes.Contains(b)) ??
LevelGenerationParams.GetBiomes().GetRandom(Rand.RandSync.Server);
float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server);
var levelData = new LevelData(
seed,
difficulty ?? Rand.Range(30.0f, 80.0f, Rand.RandSync.Server),
Rand.Range(0.0f, 1.0f, Rand.RandSync.Server),
generationParams,
biome)
biome);
if (type == LevelType.LocationConnection)
{
HasBeaconStation = beaconRng < 0.5f,
IsBeaconActive = beaconRng > 0.25f
};
GameMain.GameSession?.GameMode?.Mission?.AdjustLevelData(levelData);
float beaconRng = Rand.Range(0.0f, 1.0f, Rand.RandSync.Server);
levelData.HasBeaconStation = beaconRng < 0.5f;
levelData.IsBeaconActive = beaconRng > 0.25f;
}
if (GameMain.GameSession?.GameMode != null)
{
foreach (Mission mission in GameMain.GameSession.GameMode.Missions)
{
mission.AdjustLevelData(levelData);
}
}
return levelData;
}
@@ -207,6 +228,13 @@ namespace Barotrauma
new XAttribute("isbeaconactive", IsBeaconActive.ToString()));
}
if (HasHuntingGrounds)
{
newElement.Add(
new XAttribute("hashuntinggrounds", HasHuntingGrounds.ToString()));
}
if (Type == LevelType.Outpost)
{
if (EventHistory.Any())
@@ -195,25 +195,25 @@ namespace Barotrauma
set;
}
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
[Serialize(100000, true), Editable]
public int MinWidth
{
get { return minWidth; }
set { minWidth = Math.Max(value, 2000); }
set { minWidth = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(100000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
[Serialize(100000, true), Editable]
public int MaxWidth
{
get { return maxWidth; }
set { maxWidth = Math.Max(value, 2000); }
set { maxWidth = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(50000, true), Editable(MinValueInt = 10000, MaxValueInt = 1000000)]
[Serialize(50000, true), Editable]
public int Height
{
get { return height; }
set { height = Math.Max(value, 2000); }
set { height = MathHelper.Clamp(value, 2000, 1000000); }
}
[Serialize(80000, true), Editable(MinValueInt = 0, MaxValueInt = 1000000)]
@@ -404,7 +404,35 @@ namespace Barotrauma
set;
}
[Serialize(300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)]
[Serialize(5, true), Editable(MinValueInt = 0, MaxValueInt = 20)]
public int AbyssIslandCount
{
get;
set;
}
[Serialize("4000,7000", true), Editable]
public Point AbyssIslandSizeMin
{
get;
set;
}
[Serialize("8000,10000", true), Editable]
public Point AbyssIslandSizeMax
{
get;
set;
}
[Serialize(0.5f, true), Editable()]
public float AbyssIslandCaveProbability
{
get;
set;
}
[Serialize(-300000, true, description: "How far below the level the sea floor is placed."), Editable(MinValueFloat = Level.MaxEntityDepth, MaxValueFloat = 0.0f)]
public int SeaFloorDepth
{
get { return seaFloorBaseDepth; }
@@ -554,7 +582,7 @@ namespace Barotrauma
var matchingLevelParams = LevelParams.FindAll(lp => lp.Type == type && lp.allowedBiomes.Any());
if (biome == null)
{
matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.Any(b => b.IsEndBiome));
matchingLevelParams = matchingLevelParams.FindAll(lp => !lp.allowedBiomes.All(b => b.IsEndBiome));
}
else
{
@@ -52,7 +52,11 @@ namespace Barotrauma
public Sprite Sprite
{
get { return spriteIndex < 0 || Prefab.Sprites.Count == 0 ? null : Prefab.Sprites[spriteIndex % Prefab.Sprites.Count]; }
get
{
var prefab = ActivePrefab?.Sprites.Count > 0 ? ActivePrefab : Prefab;
return spriteIndex < 0 || prefab.Sprites.Count == 0 ? null : prefab.Sprites[spriteIndex % prefab.Sprites.Count];
}
}
Vector2 ISpatialEntity.Position => new Vector2(Position.X, Position.Y);
@@ -63,6 +67,8 @@ namespace Barotrauma
public Submarine Submarine => null;
public Level.Cave ParentCave;
public LevelObject(LevelObjectPrefab prefab, Vector3 position, float scale, float rotation = 0.0f)
{
ActivePrefab = Prefab = prefab;
@@ -110,6 +116,19 @@ namespace Barotrauma
Triggers.Add(newTrigger);
}
if (spriteIndex == -1)
{
foreach (var overrideProperties in prefab.OverrideProperties)
{
if (overrideProperties == null) { continue; }
if (overrideProperties.Sprites.Count > 0)
{
spriteIndex = Rand.Int(overrideProperties.Sprites.Count, Rand.RandSync.Server);
break;
}
}
}
NeedsUpdate = NeedsNetworkSyncing || (Triggers != null && Triggers.Any()) || Prefab.PhysicsBodyTriggerIndex > -1;
InitProjSpecific();
@@ -151,10 +151,10 @@ namespace Barotrauma
}
}
availableSpawnPositions.Clear();
foreach (Level.Cave cave in level.Caves)
{
availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.CaveWall)));
availableSpawnPositions.Clear();
suitableSpawnPositions.Clear();
spawnPositionWeights.Clear();
@@ -171,7 +171,7 @@ namespace Barotrauma
for (int i = 0; i < cave.CaveGenerationParams.LevelObjectAmount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs);
LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs, requireCaveSpecificOverride: true);
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
@@ -184,19 +184,63 @@ namespace Barotrauma
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level);
PlaceObject(prefab, spawnPosition, level, cave);
if (prefab.MaxCount < amount)
{
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
if (objects.Count(o => o.Prefab == prefab && o.ParentCave == cave) >= prefab.MaxCount)
{
availablePrefabs.Remove(prefab);
}
}
}
}
}
}
private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level)
public void PlaceNestObjects(Level level, Level.Cave cave, Vector2 nestPosition, float nestRadius, int objectAmount)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(level.Seed));
var availablePrefabs = new List<LevelObjectPrefab>(LevelObjectPrefab.List.FindAll(p => p.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.NestWall)));
Dictionary<LevelObjectPrefab, List<SpawnPosition>> suitableSpawnPositions = new Dictionary<LevelObjectPrefab, List<SpawnPosition>>();
Dictionary<LevelObjectPrefab, List<float>> spawnPositionWeights = new Dictionary<LevelObjectPrefab, List<float>>();
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
var caveCells = cave.Tunnels.SelectMany(t => t.Cells);
List<VoronoiCell> caveWallCells = new List<VoronoiCell>();
foreach (var edge in caveCells.SelectMany(c => c.Edges))
{
if (!edge.NextToCave) { continue; }
if (MathUtils.LineSegmentToPointDistanceSquared(edge.Point1.ToPoint(), edge.Point2.ToPoint(), nestPosition.ToPoint()) > nestRadius * nestRadius) { continue; }
if (edge.Cell1?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell1); }
if (edge.Cell2?.CellType == CellType.Solid) { caveWallCells.Add(edge.Cell2); }
}
availableSpawnPositions.AddRange(GetAvailableSpawnPositions(caveWallCells.Distinct(), LevelObjectPrefab.SpawnPosType.CaveWall));
for (int i = 0; i < objectAmount; i++)
{
//get a random prefab and find a place to spawn it
LevelObjectPrefab prefab = GetRandomPrefab(cave.CaveGenerationParams, availablePrefabs, requireCaveSpecificOverride: false);
if (prefab == null) { continue; }
if (!suitableSpawnPositions.ContainsKey(prefab))
{
suitableSpawnPositions.Add(prefab,
availableSpawnPositions.Where(sp =>
sp.Length >= prefab.MinSurfaceWidth &&
(sp.Alignment == Alignment.Any || prefab.Alignment.HasFlag(sp.Alignment))).ToList());
spawnPositionWeights.Add(prefab,
suitableSpawnPositions[prefab].Select(sp => sp.GetSpawnProbability(prefab)).ToList());
}
SpawnPosition spawnPosition = ToolBox.SelectWeightedRandom(suitableSpawnPositions[prefab], spawnPositionWeights[prefab], Rand.RandSync.Server);
if (spawnPosition == null && prefab.SpawnPos != LevelObjectPrefab.SpawnPosType.None) { continue; }
PlaceObject(prefab, spawnPosition, level);
if (objects.Count(o => o.Prefab == prefab) >= prefab.MaxCount)
{
availablePrefabs.Remove(prefab);
}
}
}
private void PlaceObject(LevelObjectPrefab prefab, SpawnPosition spawnPosition, Level level, Level.Cave parentCave = null)
{
float rotation = 0.0f;
if (prefab.AlignWithSurface && spawnPosition.Normal.LengthSquared() > 0.001f && spawnPosition != null)
@@ -228,6 +272,7 @@ namespace Barotrauma
var newObject = new LevelObject(prefab,
new Vector3(position, Rand.Range(prefab.DepthRange.X, prefab.DepthRange.Y, Rand.RandSync.Server)), Rand.Range(prefab.MinSize, prefab.MaxSize, Rand.RandSync.Server), rotation);
AddObject(newObject, level);
newObject.ParentCave = parentCave;
foreach (LevelObjectPrefab.ChildObject child in prefab.ChildObjects)
{
@@ -237,7 +282,7 @@ namespace Barotrauma
var matchingPrefabs = LevelObjectPrefab.List.Where(p => child.AllowedNames.Contains(p.Name));
int prefabCount = matchingPrefabs.Count();
var childPrefab = prefabCount == 0 ? null : matchingPrefabs.ElementAt(Rand.Range(0, prefabCount, Rand.RandSync.Server));
if (childPrefab == null) continue;
if (childPrefab == null) { continue; }
Vector2 childPos = position + edgeDir * Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server) * prefab.MinSurfaceWidth;
@@ -247,6 +292,7 @@ namespace Barotrauma
rotation + Rand.Range(childPrefab.RandomRotationRad.X, childPrefab.RandomRotationRad.Y, Rand.RandSync.Server));
AddObject(childObject, level);
childObject.ParentCave = parentCave;
}
}
}
@@ -372,7 +418,7 @@ namespace Barotrauma
return objects;
}
private readonly static List<LevelObject> objectsInRange = new List<LevelObject>();
private readonly static HashSet<LevelObject> objectsInRange = new HashSet<LevelObject>();
public IEnumerable<LevelObject> GetAllObjects(Vector2 worldPosition, float radius)
{
var minIndices = GetGridIndices(worldPosition - Vector2.One * radius);
@@ -391,10 +437,10 @@ namespace Barotrauma
{
for (int y = minIndices.Y; y <= maxIndices.Y; y++)
{
if (objectGrid[x, y] == null) continue;
if (objectGrid[x, y] == null) { continue; }
foreach (LevelObject obj in objectGrid[x, y])
{
if (!objectsInRange.Contains(obj)) objectsInRange.Add(obj);
objectsInRange.Add(obj);
}
}
}
@@ -402,7 +448,7 @@ namespace Barotrauma
return objectsInRange;
}
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType)
private List<SpawnPosition> GetAvailableSpawnPositions(IEnumerable<VoronoiCell> cells, LevelObjectPrefab.SpawnPosType spawnPosType, bool checkFlags = true)
{
List<LevelObjectPrefab.SpawnPosType> spawnPosTypes = new List<LevelObjectPrefab.SpawnPosType>(4);
List<SpawnPosition> availableSpawnPositions = new List<SpawnPosition>();
@@ -498,12 +544,12 @@ namespace Barotrauma
availablePrefabs.Select(p => p.GetCommonness(generationParams)).ToList(), Rand.RandSync.Server);
}
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs)
private LevelObjectPrefab GetRandomPrefab(CaveGenerationParams caveParams, IList<LevelObjectPrefab> availablePrefabs, bool requireCaveSpecificOverride)
{
if (availablePrefabs.Sum(p => p.GetCommonness(caveParams)) <= 0.0f) { return null; }
if (availablePrefabs.Sum(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)) <= 0.0f) { return null; }
return ToolBox.SelectWeightedRandom(
availablePrefabs,
availablePrefabs.Select(p => p.GetCommonness(caveParams)).ToList(), Rand.RandSync.Server);
availablePrefabs.Select(p => p.GetCommonness(caveParams, requireCaveSpecificOverride)).ToList(), Rand.RandSync.Server);
}
public override void Remove()
@@ -37,11 +37,12 @@ namespace Barotrauma
MainPathWall = 1,
SidePathWall = 2,
CaveWall = 4,
RuinWall = 8,
SeaFloor = 16,
MainPath = 32,
LevelStart = 64,
LevelEnd = 128,
NestWall = 8,
RuinWall = 16,
SeaFloor = 32,
MainPath = 64,
LevelStart = 128,
LevelEnd = 256,
Wall = MainPathWall | SidePathWall | CaveWall,
}
@@ -319,7 +320,7 @@ namespace Barotrauma
}
else if (List.Any())
{
DebugConsole.NewMessage($"Loading additional level object prefabs from file '{configPath}'");
DebugConsole.Log($"Loading additional level object prefabs from file '{configPath}'");
}
foreach (XElement subElement in mainElement.Elements())
{
@@ -442,14 +443,14 @@ namespace Barotrauma
partial void InitProjSpecific(XElement element);
public float GetCommonness(CaveGenerationParams generationParams)
public float GetCommonness(CaveGenerationParams generationParams, bool requireCaveSpecificOverride = true)
{
if (generationParams?.Identifier != null &&
OverrideCommonness.TryGetValue(generationParams.Identifier, out float commonness))
{
return commonness;
}
return 0.0f;
return requireCaveSpecificOverride ? 0.0f : Commonness;
}
public float GetCommonness(LevelGenerationParams generationParams)
@@ -34,44 +34,38 @@ namespace Barotrauma
public Action<LevelTrigger, Entity> OnTriggered;
private PhysicsBody physicsBody;
/// <summary>
/// Effects applied to entities that are inside the trigger
/// </summary>
private List<StatusEffect> statusEffects = new List<StatusEffect>();
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
/// <summary>
/// Attacks applied to entities that are inside the trigger
/// </summary>
private List<Attack> attacks = new List<Attack>();
private readonly List<Attack> attacks = new List<Attack>();
private float cameraShake;
private readonly float cameraShake;
private Vector2 unrotatedForce;
private float forceFluctuationTimer, currentForceFluctuation = 1.0f;
private HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private TriggererType triggeredBy;
private readonly TriggererType triggeredBy;
private float randomTriggerInterval;
private float randomTriggerProbability;
private readonly float randomTriggerInterval;
private readonly float randomTriggerProbability;
private float randomTriggerTimer;
private float triggeredTimer;
//how far away this trigger can activate other triggers from
private float triggerOthersDistance;
private HashSet<string> tags = new HashSet<string>();
private readonly HashSet<string> tags = new HashSet<string>();
//other triggers have to have at least one of these tags to trigger this one
private HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
private readonly HashSet<string> allowedOtherTriggerTags = new HashSet<string>();
/// <summary>
/// How long the trigger stays in the triggered state after triggerers have left
/// </summary>
private float stayTriggeredDelay;
private readonly float stayTriggeredDelay;
public LevelTrigger ParentTrigger;
@@ -88,30 +82,24 @@ namespace Barotrauma
set
{
worldPosition = value;
physicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), physicsBody.Rotation);
PhysicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), PhysicsBody.Rotation);
}
}
public float Rotation
{
get { return physicsBody == null ? 0.0f : physicsBody.Rotation; }
get { return PhysicsBody == null ? 0.0f : PhysicsBody.Rotation; }
set
{
if (physicsBody == null) return;
physicsBody.SetTransform(physicsBody.Position, value);
if (PhysicsBody == null) return;
PhysicsBody.SetTransform(PhysicsBody.Position, value);
CalculateDirectionalForce();
}
}
public PhysicsBody PhysicsBody
{
get { return physicsBody; }
}
public PhysicsBody PhysicsBody { get; private set; }
public float TriggerOthersDistance
{
get { return triggerOthersDistance; }
}
public float TriggerOthersDistance { get; private set; }
public IEnumerable<Entity> Triggerers
{
@@ -153,7 +141,7 @@ namespace Barotrauma
private set;
}
private TriggerForceMode forceMode;
private readonly TriggerForceMode forceMode;
public TriggerForceMode ForceMode
{
get { return forceMode; }
@@ -198,6 +186,9 @@ namespace Barotrauma
get;
set;
}
private bool triggeredOnce;
private readonly bool triggerOnce;
public LevelTrigger(XElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "")
{
@@ -206,20 +197,20 @@ namespace Barotrauma
worldPosition = position;
if (element.Attributes("radius").Any() || element.Attributes("width").Any() || element.Attributes("height").Any())
{
physicsBody = new PhysicsBody(element, scale)
PhysicsBody = new PhysicsBody(element, scale)
{
CollisionCategories = Physics.CollisionLevel,
CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall
};
physicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision;
physicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation;
physicsBody.FarseerBody.SetIsSensor(true);
physicsBody.FarseerBody.BodyType = BodyType.Static;
physicsBody.FarseerBody.BodyType = BodyType.Kinematic;
PhysicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision;
PhysicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation;
PhysicsBody.FarseerBody.SetIsSensor(true);
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
PhysicsBody.FarseerBody.BodyType = BodyType.Kinematic;
ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.radius, PhysicsBody.width / 2.0f), PhysicsBody.height / 2.0f));
physicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation);
PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation);
}
cameraShake = element.GetAttributeFloat("camerashake", 0.0f);
@@ -227,6 +218,8 @@ namespace Barotrauma
InfectIdentifier = element.GetAttributeString("infectidentifier", null);
InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f);
triggerOnce = element.GetAttributeBool("triggeronce", false);
stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f);
randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f);
randomTriggerProbability = element.GetAttributeFloat("randomtriggerprobability", 0.0f);
@@ -256,7 +249,7 @@ namespace Barotrauma
DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type.");
}
UpdateCollisionCategories();
triggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f);
var tagsArray = element.GetAttributeStringArray("tags", new string[0]);
foreach (string tag in tagsArray)
@@ -283,11 +276,14 @@ namespace Barotrauma
case "attack":
case "damage":
var attack = new Attack(subElement, string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : "LevelTrigger in " + parentDebugName);
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
if (!triggerOnce)
{
attack.Afflictions.Add(affliction, null);
var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step);
attack.Afflictions.Clear();
foreach (Affliction affliction in multipliedAfflictions)
{
attack.Afflictions.Add(affliction, null);
}
}
attacks.Add(attack);
break;
@@ -300,14 +296,14 @@ namespace Barotrauma
private void UpdateCollisionCategories()
{
if (physicsBody == null) return;
if (PhysicsBody == null) return;
var collidesWith = Physics.CollisionNone;
if (triggeredBy.HasFlag(TriggererType.Character) || triggeredBy.HasFlag(TriggererType.Creature)) collidesWith |= Physics.CollisionCharacter;
if (triggeredBy.HasFlag(TriggererType.Item)) collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile;
if (triggeredBy.HasFlag(TriggererType.Submarine)) collidesWith |= Physics.CollisionWall;
if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; }
if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; }
if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; }
physicsBody.CollidesWith = collidesWith;
PhysicsBody.CollidesWith = collidesWith;
}
private void CalculateDirectionalForce()
@@ -362,7 +358,7 @@ namespace Barotrauma
private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact)
{
Entity entity = GetEntity(fixtureB);
if (entity == null) return;
if (entity == null) { return; }
if (entity is Character character &&
(!character.Enabled || character.Removed) &&
@@ -376,22 +372,25 @@ namespace Barotrauma
//check if there are contacts with any other fixture of the trigger
//(the OnSeparation callback happens when two fixtures separate,
//e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body)
ContactEdge contactEdge = fixtureA.Body.ContactList;
while (contactEdge != null)
foreach (Fixture fixture in PhysicsBody.FarseerBody.FixtureList)
{
if (contactEdge.Contact != null &&
contactEdge.Contact.Enabled &&
contactEdge.Contact.IsTouching)
ContactEdge contactEdge = fixture.Body.ContactList;
while (contactEdge != null)
{
if (contactEdge.Contact.FixtureA != fixtureA && contactEdge.Contact.FixtureB != fixtureA)
if (contactEdge.Contact != null &&
contactEdge.Contact.Enabled &&
contactEdge.Contact.IsTouching)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
if (otherEntity == entity) { return; }
if (contactEdge.Contact.FixtureA != fixture && contactEdge.Contact.FixtureB != fixture)
{
var otherEntity = GetEntity(contactEdge.Contact.FixtureB == fixtureB ?
contactEdge.Contact.FixtureB :
contactEdge.Contact.FixtureA);
if (otherEntity == entity) { return; }
}
}
contactEdge = contactEdge.Next;
}
contactEdge = contactEdge.Next;
}
if (triggerers.Contains(entity))
@@ -403,10 +402,10 @@ namespace Barotrauma
private Entity GetEntity(Fixture fixture)
{
if (fixture.Body == null || fixture.Body.UserData == null) return null;
if (fixture.Body.UserData is Entity entity) return entity;
if (fixture.Body.UserData is Limb limb) return limb.character;
if (fixture.Body.UserData is SubmarineBody subBody) return subBody.Submarine;
if (fixture.Body == null || fixture.Body.UserData == null) { return null; }
if (fixture.Body.UserData is Entity entity) { return entity; }
if (fixture.Body.UserData is Limb limb) { return limb.character; }
if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; }
return null;
}
@@ -416,15 +415,15 @@ namespace Barotrauma
/// </summary>
public void OtherTriggered(LevelObject levelObject, LevelTrigger otherTrigger)
{
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) return;
if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; }
//check if the other trigger has appropriate tags
if (allowedOtherTriggerTags.Count > 0)
{
if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) return;
if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) { return; }
}
if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.triggerOthersDistance * otherTrigger.triggerOthersDistance)
if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.TriggerOthersDistance * otherTrigger.TriggerOthersDistance)
{
bool wasAlreadyTriggered = IsTriggered;
triggeredTimer = stayTriggeredDelay;
@@ -441,10 +440,10 @@ namespace Barotrauma
triggerers.RemoveWhere(t => t.Removed);
if (physicsBody != null)
if (PhysicsBody != null)
{
//failsafe to ensure triggerers get removed when they're far from the trigger
float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(physicsBody.GetMaxExtent() * 5), 5000.0f);
float maxExtent = Math.Max(ConvertUnits.ToDisplayUnits(PhysicsBody.GetMaxExtent() * 5), 5000.0f);
triggerers.RemoveWhere(t =>
{
return Vector2.Distance(t.WorldPosition, WorldPosition) > maxExtent;
@@ -500,17 +499,43 @@ namespace Barotrauma
}
}
if (triggerOnce)
{
if (triggeredOnce) { return; }
if (triggerers.Count > 0) { triggeredOnce = true; }
}
foreach (Entity triggerer in triggerers)
{
foreach (StatusEffect effect in statusEffects)
{
if (triggerer is Character)
Vector2? position = null;
if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = WorldPosition; }
if (triggerer is Character character)
{
effect.Apply(effect.type, deltaTime, triggerer, (Character)triggerer);
effect.Apply(effect.type, deltaTime, triggerer, character, position);
if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null)
{
foreach (Item item in character.Inventory.AllItemsMod)
{
if (item.ContainedItems == null) { continue; }
foreach (Item containedItem in item.ContainedItems)
{
effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position);
}
}
}
}
else if (triggerer is Item)
else if (triggerer is Item item)
{
effect.Apply(effect.type, deltaTime, triggerer, ((Item)triggerer).AllPropertyObjects);
effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(worldPosition, targets);
effect.Apply(effect.type, deltaTime, triggerer, targets);
}
}
@@ -528,7 +553,7 @@ namespace Barotrauma
float structureDamage = attack.GetStructureDamage(deltaTime);
if (structureDamage > 0.0f)
{
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, damageLevelWalls: false);
Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f);
}
}
@@ -618,7 +643,7 @@ namespace Barotrauma
public Vector2 GetWaterFlowVelocity()
{
if (Force == Vector2.Zero) return Vector2.Zero;
if (Force == Vector2.Zero || ForceMode == TriggerForceMode.LimitVelocity) { return Vector2.Zero; }
Vector2 vel = Force;
if (ForceMode == TriggerForceMode.Acceleration)
@@ -36,7 +36,16 @@ namespace Barotrauma
}
}
public float WallDamageOnTouch;
private float wallDamageOnTouch;
public float WallDamageOnTouch
{
get { return wallDamageOnTouch; }
set
{
Cells.ForEach(c => c.DoesDamage = !MathUtils.NearlyEqual(value, 0.0f));
wallDamageOnTouch = value;
}
}
public float MoveSpeed;
@@ -1,9 +1,14 @@
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
#if DEBUG
using System.Xml;
#else
using Barotrauma.IO;
#endif
namespace Barotrauma.RuinGeneration
{
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.IO;
namespace Barotrauma
{
@@ -98,6 +99,8 @@ namespace Barotrauma
LinkedSubmarine sl = CreateDummy(mainSub, doc.Root, position);
sl.filePath = filePath;
sl.saveElement = doc.Root;
sl.saveElement.Name = "LinkedSubmarine";
return sl;
}
@@ -132,7 +135,11 @@ namespace Barotrauma
public override MapEntity Clone()
{
return CreateDummy(Submarine, filePath, Position);
XElement cloneElement = new XElement(saveElement);
LinkedSubmarine sl = CreateDummy(Submarine, cloneElement, Position);
sl.saveElement = cloneElement;
sl.filePath = filePath;
return sl;
}
private void GenerateWallVertices(XElement rootElement)
@@ -232,6 +239,7 @@ namespace Barotrauma
IdRemap parentRemap = new IdRemap(Submarine.Info.SubmarineElement, Submarine.IdOffset);
sub = Submarine.Load(info, false, parentRemap);
sub.Info.SubmarineClass = Submarine.Info.SubmarineClass;
IdRemap childRemap = new IdRemap(saveElement, sub.IdOffset);
@@ -289,7 +297,8 @@ namespace Barotrauma
{
originalMyPortID = myPort.Item.ID;
myPort.Undock();
myPort.Undock(applyEffects: false);
myPort.DockingDir = 0;
//something else is already docked to the port this sub should be docked to
//may happen if a shuttle is lost, another vehicle docked to where the shuttle used to be,
@@ -313,7 +322,7 @@ namespace Barotrauma
sub.SetPosition((linkedPort.Item.WorldPosition - portDiff) - offset);
myPort.Dock(linkedPort);
myPort.Lock(true);
myPort.Lock(true, applyEffects: false);
}
}
@@ -324,7 +333,7 @@ namespace Barotrauma
if (wall.Submarine != sub) { continue; }
for (int i = 0; i < wall.SectionCount; i++)
{
wall.AddDamage(i, -wall.MaxHealth);
wall.SetDamage(i, 0, createNetworkEvent: false);
}
}
foreach (Hull hull in Hull.hullList)
@@ -349,15 +358,20 @@ namespace Barotrauma
{
var doc = SubmarineInfo.OpenFile(filePath);
saveElement = doc.Root;
saveElement.Name = "LinkedSubmarine";
saveElement.Add(new XAttribute("filepath", filePath));
}
else
{
saveElement = this.saveElement;
}
saveElement.Name = "LinkedSubmarine";
if (saveElement.Attribute("pos") != null) saveElement.Attribute("pos").Remove();
if (saveElement.Attribute("previewimage") != null)
{
saveElement.Attribute("previewimage").Remove();
}
if (saveElement.Attribute("pos") != null) { saveElement.Attribute("pos").Remove(); }
saveElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(Position - Submarine.HiddenSubPosition)));
var linkedPort = linkedTo.FirstOrDefault(lt => (lt is Item) && ((Item)lt).GetComponent<DockingPort>() != null);
@@ -33,8 +33,9 @@ namespace Barotrauma
{
OriginalContainerID = item.OriginalContainerID;
}
OriginalID = item.ID;
ModuleIndex = (ushort)item.OriginalModuleIndex;
ModuleIndex = (ushort) item.OriginalModuleIndex;
Identifier = item.prefab.Identifier;
}
@@ -42,6 +43,7 @@ namespace Barotrauma
{
return obj.OriginalID == OriginalID && obj.OriginalContainerID == OriginalContainerID && obj.ModuleIndex == ModuleIndex && obj.Identifier == Identifier;
}
public bool Matches(Item item)
{
if (item.OriginalContainerID != Entity.NullEntityID)
@@ -56,13 +58,17 @@ namespace Barotrauma
}
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
private string baseName;
private int nameFormatIndex;
public bool Discovered;
public int TypeChangeTimer;
public readonly Dictionary<LocationTypeChange.Requirement, int> ProximityTimer = new Dictionary<LocationTypeChange.Requirement, int>();
public (LocationTypeChange typeChange, int delay, MissionPrefab parentMission)? PendingLocationTypeChange;
public int LocationTypeChangeCooldown;
public readonly int ZoneIndex;
public string BaseName { get => baseName; }
@@ -80,14 +86,76 @@ namespace Barotrauma
public Reputation Reputation { get; set; }
public int[] ProximityTime { get; private set; }
public int TurnsInRadiation { get; set; }
#region Store
private const float StoreMaxReputationModifier = 0.1f;
private const float StoreSellPriceModifier = 0.8f;
private const float MechanicalMaxDiscountPercentage = 50.0f;
private const float DailySpecialPriceModifier = 0.9f;
private const float RequestGoodPriceModifier = 1.5f;
public const int StoreInitialBalance = 5000;
public int StoreCurrentBalance { get; set; }
/// <summary>
/// In percentages
/// </summary>
private const int StorePriceModifierRange = 5;
/// <summary>
/// In percentages. Larger values make buying more expensive and selling less profitable, and vice versa.
/// </summary>
public int StorePriceModifier { get; private set; }
public Color BalanceColor => ActiveStoreBalanceStatus.Color;
public StoreBalanceStatus ActiveStoreBalanceStatus { get; private set; }
private static StoreBalanceStatus DefaultBalanceStatus { get; } = new StoreBalanceStatus(1.0f, 1.0f, Color.White);
private static List<StoreBalanceStatus> StoreBalanceStatuses { get; } = new List<StoreBalanceStatus>
{
new StoreBalanceStatus(0.5f, 0.75f, Color.Orange),
new StoreBalanceStatus(0.25f, 0.2f, Color.Red),
};
public struct StoreBalanceStatus
{
public float PercentageOfInitialBalance { get; }
public float SellPriceModifier { get; }
public Color Color { get; }
public StoreBalanceStatus(float percentage, float sellPriceModifier, Color color)
{
PercentageOfInitialBalance = percentage;
SellPriceModifier = sellPriceModifier;
Color = color;
}
}
private int storeCurrentBalance;
public int StoreCurrentBalance
{
get
{
return storeCurrentBalance;
}
set
{
storeCurrentBalance = value;
ActiveStoreBalanceStatus = GetStoreBalanceStatus(value);
}
}
public List<PurchasedItem> StoreStock { get; set; }
public List<ItemPrefab> DailySpecials { get; } = new List<ItemPrefab>();
public List<ItemPrefab> RequestedGoods { get; } = new List<ItemPrefab>();
/// <summary>
/// How many map progress steps it takes before the discounts should be updated.
/// </summary>
private const int SpecialsUpdateInterval = 3;
private const int DailySpecialsCount = 3;
private const int RequestedGoodsCount = 3;
private int StepsSinceSpecialsUpdated { get; set; }
#endregion
private const float MechanicalMaxDiscountPercentage = 50.0f;
private readonly List<TakenItem> takenItems = new List<TakenItem>();
public IEnumerable<TakenItem> TakenItems
@@ -106,12 +174,11 @@ namespace Barotrauma
{
get
{
availableMissions.RemoveAll(m => m.Completed || m.Failed);
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
return availableMissions;
}
}
public Mission SelectedMission
{
get;
@@ -152,6 +219,10 @@ namespace Barotrauma
public string LastTypeChangeMessage;
public int TimeSinceLastTypeChange;
public bool IsGateBetweenBiomes;
private struct LoadedMission
{
public MissionPrefab MissionPrefab { get; }
@@ -175,29 +246,49 @@ namespace Barotrauma
return $"Location ({Name ?? "null"})";
}
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, IEnumerable<Location> existingLocations = null)
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType? forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
Type = LocationType.Random(rand, zone, requireOutpost);
Type = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
Name = RandomName(Type, rand, existingLocations);
MapPosition = mapPosition;
PortraitId = ToolBox.StringToInt(Name);
Connections = new List<LocationConnection>();
ProximityTime = new int[Type.CanChangeTo.Count];
Connections = new List<LocationConnection>();
}
public Location(XElement element)
{
string locationType = element.GetAttributeString("type", "");
Type = LocationType.List.Find(lt => lt.Identifier.Equals(locationType, StringComparison.OrdinalIgnoreCase));
bool typeNotFound = false;
if (Type == null)
{
DebugConsole.AddWarning($"Could not find location type \"{locationType}\". Using location type \"None\" instead.");
Type = LocationType.List.Find(lt => lt.Identifier.Equals("None", StringComparison.OrdinalIgnoreCase));
Type ??= LocationType.List.First();
typeNotFound = true;
}
baseName = element.GetAttributeString("basename", "");
Name = element.GetAttributeString("name", "");
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
TypeChangeTimer = element.GetAttributeInt("changetimer", 0);
Discovered = element.GetAttributeBool("discovered", false);
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
ProximityTime = element.GetAttributeIntArray("proximitytime", new int[Type.CanChangeTo.Count]);
if (ProximityTime.Length != Type.CanChangeTo.Count) { ProximityTime = new int[Type.CanChangeTo.Count]; }
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
MechanicalPriceMultiplier = element.GetAttributeFloat("mechanicalpricemultipler", 1.0f);
TurnsInRadiation = element.GetAttributeInt(nameof(TurnsInRadiation).ToLower(), 0);
if (!typeNotFound)
{
for (int i = 0; i < Type.CanChangeTo.Count; i++)
{
for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
{
ProximityTimer.Add(Type.CanChangeTo[i].Requirements[j], element.GetAttributeInt("proximitytimer" + i + "-" + j, 0));
}
}
LoadLocationTypeChange(element);
}
string[] takenItemStr = element.GetAttributeStringArray("takenitems", new string[0]);
foreach (string takenItem in takenItemStr)
@@ -237,16 +328,42 @@ namespace Barotrauma
LevelData = new LevelData(element.Element("Level"));
PortraitId = ToolBox.StringToInt(Name);
if (element.GetChildElement("store") is XElement storeElement)
{
StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance);
StoreStock = LoadStoreStock(storeElement);
}
LoadStore(element);
LoadMissions(element);
}
public void LoadLocationTypeChange(XElement locationElement)
{
TimeSinceLastTypeChange = locationElement.GetAttributeInt("timesincelasttypechange", 0);
LocationTypeChangeCooldown = locationElement.GetAttributeInt("locationtypechangecooldown", 0);
foreach (XElement subElement in locationElement.Elements())
{
switch (subElement.Name.ToString())
{
case "pendinglocationtypechange":
int timer = subElement.GetAttributeInt("timer", 0);
if (subElement.Attribute("index") != null)
{
int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
}
else
{
string missionIdentifier = subElement.GetAttributeString("missionidentifier", "");
var mission = MissionPrefab.List.Find(mp => mp.Identifier.Equals(missionIdentifier, StringComparison.OrdinalIgnoreCase));
if (mission == null)
{
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found.");
continue;
}
PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
}
break;
}
}
}
public void LoadMissions(XElement locationElement)
{
if (locationElement.GetChildElement("missions") is XElement missionsElement)
@@ -266,9 +383,9 @@ namespace Barotrauma
}
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, IEnumerable<Location> existingLocations = null)
public static Location CreateRandom(Vector2 position, int? zone, Random rand, bool requireOutpost, LocationType? forceLocationType = null, IEnumerable<Location> existingLocations = null)
{
return new Location(position, zone, rand, requireOutpost, existingLocations);
return new Location(position, zone, rand, requireOutpost, forceLocationType, existingLocations);
}
public void ChangeType(LocationType newType)
@@ -278,15 +395,34 @@ namespace Barotrauma
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
Type = newType;
ProximityTime = new int[Type.CanChangeTo.Count];
Name = Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
Name = Type.NameFormats == null ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
if (Type.MissionIdentifiers.Any())
{
UnlockMissionByIdentifier(Type.MissionIdentifiers.GetRandom());
}
if (Type.MissionTags.Any())
{
UnlockMissionByTag(Type.MissionTags.GetRandom());
}
CreateStore(force: true);
}
public void UnlockMission(MissionPrefab missionPrefab, LocationConnection connection)
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
var mission = InstantiateMission(missionPrefab, ref connection);
var mission = InstantiateMission(missionPrefab, connection);
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
#endif
}
public void UnlockMission(MissionPrefab missionPrefab)
{
if (AvailableMissions.Any(m => m.Prefab == missionPrefab)) { return; }
var mission = InstantiateMission(missionPrefab);
availableMissions.Add(mission);
#if CLIENT
GameMain.GameSession?.Campaign?.CampaignUI?.RefreshLocationInfo();
@@ -304,8 +440,7 @@ namespace Barotrauma
}
else
{
LocationConnection connection = null;
var mission = InstantiateMission(missionPrefab, ref connection);
var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
//don't allow duplicate missions in the same connection
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
{
@@ -332,14 +467,13 @@ namespace Barotrauma
var unusedMissions = matchingMissions.Where(m => !availableMissions.Any(mission => mission.Prefab == m));
if (unusedMissions.Any())
{
var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this))));
var suitableMissions = unusedMissions.Where(m => Connections.Any(c => m.IsAllowed(this, c.OtherLocation(this)) || m.IsAllowed(this, this)));
if (!suitableMissions.Any())
{
suitableMissions = unusedMissions;
}
LocationConnection connection = null;
MissionPrefab missionPrefab = suitableMissions.GetRandom();
var mission = InstantiateMission(missionPrefab, ref connection);
MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(suitableMissions.ToList(), suitableMissions.Select(m => (float)m.Commonness).ToList(), Rand.RandSync.Unsynced);
var mission = InstantiateMission(missionPrefab, out LocationConnection connection);
//don't allow duplicate missions in the same connection
if (AvailableMissions.Any(m => m.Prefab == missionPrefab && m.Locations.Contains(mission.Locations[0]) && m.Locations.Contains(mission.Locations[1])))
{
@@ -360,8 +494,14 @@ namespace Barotrauma
return null;
}
private Mission InstantiateMission(MissionPrefab prefab, ref LocationConnection connection)
private Mission InstantiateMission(MissionPrefab prefab, out LocationConnection connection)
{
if (prefab.IsAllowed(this, this))
{
connection = null;
return InstantiateMission(prefab);
}
var suitableConnections = Connections.Where(c => prefab.IsAllowed(this, c.OtherLocation(this)));
if (!suitableConnections.Any())
{
@@ -371,21 +511,33 @@ namespace Barotrauma
connection = ToolBox.SelectWeightedRandom(
suitableConnections.ToList(),
suitableConnections.Select(c => (c.Passed ? 1.0f : 5.0f) / Math.Max(availableMissions.Count(m => m.Locations.Contains(c.OtherLocation(this))), 1.0f)).ToList(),
Rand.RandSync.Unsynced);
Rand.RandSync.Unsynced);
return InstantiateMission(prefab, connection);
}
private Mission InstantiateMission(MissionPrefab prefab, LocationConnection connection)
{
Location destination = connection.OtherLocation(this);
var mission = prefab.Instantiate(new Location[] { this, destination });
mission.AdjustLevelData(connection.LevelData);
return mission;
}
private Mission InstantiateMission(MissionPrefab prefab)
{
var mission = prefab.Instantiate(new Location[] { this, this });
mission.AdjustLevelData(LevelData);
return mission;
}
public void InstantiateLoadedMissions(Map map)
{
availableMissions.Clear();
if (loadedMissions == null || loadedMissions.None()) { return; }
foreach (LoadedMission loadedMission in loadedMissions)
{
Location destination = null;
Location destination;
if (loadedMission.DestinationIndex >= 0 && loadedMission.DestinationIndex < map.Locations.Count)
{
destination = map.Locations[loadedMission.DestinationIndex];
@@ -410,6 +562,23 @@ namespace Barotrauma
SelectedMissionIndex = -1;
}
public bool HasOutpost()
{
if (!Type.HasOutpost) { return false; }
return !IsCriticallyRadiated();
}
public bool IsCriticallyRadiated()
{
if (GameMain.GameSession is { Campaign: { Map: { } map } })
{
return TurnsInRadiation > map.Radiation.Params.CriticalRadiationThreshold;
}
return false;
}
public IEnumerable<Mission> GetMissionsInConnection(LocationConnection connection)
{
System.Diagnostics.Debug.Assert(Connections.Contains(connection));
@@ -456,6 +625,61 @@ namespace Barotrauma
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
}
public void LoadStore(XElement locationElement)
{
StoreStock?.Clear();
DailySpecials.Clear();
RequestedGoods.Clear();
if (locationElement.GetChildElement("store") is XElement storeElement)
{
StoreCurrentBalance = storeElement.GetAttributeInt("balance", StoreInitialBalance);
StorePriceModifier = storeElement.GetAttributeInt("pricemodifier", 0);
StoreStock ??= new List<PurchasedItem>();
foreach (XElement stockElement in storeElement.GetChildElements("stock"))
{
var id = stockElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var qty = stockElement.GetAttributeInt("qty", 0);
if (qty < 1) { continue; }
StoreStock.Add(new PurchasedItem(prefab, qty));
}
StepsSinceSpecialsUpdated = storeElement.GetAttributeInt("stepssincespecialsupdated", 0);
if (storeElement.GetChildElement("dailyspecials") is XElement specialsElement)
{
var loadedDailySpecials = LoadStoreSpecials(specialsElement);
DailySpecials.AddRange(loadedDailySpecials);
}
if (storeElement.GetChildElement("requestedgoods") is XElement goodsElement)
{
var loadedRequestedGoods = LoadStoreSpecials(goodsElement);
RequestedGoods.AddRange(loadedRequestedGoods);
}
static List<ItemPrefab> LoadStoreSpecials(XElement element)
{
List<ItemPrefab> specials = new List<ItemPrefab>();
foreach (var childElement in element.GetChildElements("item"))
{
var id = childElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Find(null, id);
if (prefab == null) { continue; }
specials.Add(prefab);
}
return specials;
}
}
}
public bool IsRadiated() => GameMain.GameSession is { Campaign: { Map: { Radiation: { Enabled: true } radiation } } } && radiation.Contains(this);
private List<PurchasedItem> CreateStoreStock()
{
var stock = new List<PurchasedItem>();
@@ -463,31 +687,28 @@ namespace Barotrauma
{
if (prefab.CanBeBoughtAtLocation(this, out PriceInfo priceInfo))
{
var quantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount :
(priceInfo.MaxAvailableAmount > 0 ? Math.Min(priceInfo.MaxAvailableAmount, 5) : 5);
int quantity = PriceInfo.DefaultAmount;
if (priceInfo.MaxAvailableAmount > 0)
{
if (priceInfo.MaxAvailableAmount > priceInfo.MinAvailableAmount)
{
quantity = Rand.Range(priceInfo.MinAvailableAmount, priceInfo.MaxAvailableAmount);
}
else
{
quantity = priceInfo.MaxAvailableAmount;
}
}
else if (priceInfo.MinAvailableAmount > 0)
{
quantity = priceInfo.MinAvailableAmount;
}
stock.Add(new PurchasedItem(prefab, quantity));
}
}
return stock;
}
public static List<PurchasedItem> LoadStoreStock(XElement storeElement)
{
var stock = new List<PurchasedItem>();
if (storeElement == null) { return stock; }
foreach (XElement stockElement in storeElement.GetChildElements("stock"))
{
var id = stockElement.GetAttributeString("id", null);
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = ItemPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var qty = stockElement.GetAttributeInt("qty", 0);
if (qty < 1) { continue; }
stock.Add(new PurchasedItem(prefab, qty));
}
return stock;
}
/// <summary>
/// Mark the items that have been taken from the outpost to prevent them from spawning when re-entering the outpost
/// </summary>
@@ -526,48 +747,70 @@ namespace Barotrauma
}
}
public int GetAdjustedItemBuyPrice(PriceInfo priceInfo)
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
/// /// <param name="considerDailySpecials">If false, the price won't be affected by <see cref="DailySpecialPriceModifier"/></param>
public int GetAdjustedItemBuyPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerDailySpecials = true)
{
// TODO: Check priceInfo.CanBeBought
priceInfo ??= item?.GetPriceInfo(this);
if (priceInfo == null) { return 0; }
var price = priceInfo.Price;
float price = priceInfo.Price;
// Adjust by random price modifier
price = ((100 + StorePriceModifier) / 100.0f) * price;
// Adjust by daily special status
if (considerDailySpecials && DailySpecials.Contains(item))
{
price = DailySpecialPriceModifier * price;
}
// Adjust by current location reputation
if (Reputation.Value > 0.0f)
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price;
}
else
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price;
}
// Item price should never go below 1 mk
return Math.Max(price, 1);
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
/// <summary>
/// If item.GetPriceInfo() returns null, this will return 0
/// </summary>
public int GetAdjustedItemBuyPrice(ItemPrefab item) => GetAdjustedItemBuyPrice(item?.GetPriceInfo(this));
public int GetAdjustedItemSellPrice(PriceInfo priceInfo)
/// <param name="priceInfo">If null, item.GetPriceInfo() will be used to get it.</param>
/// <param name="considerRequestedGoods">If false, the price won't be affected by <see cref="RequestGoodPriceModifier"/></param>
public int GetAdjustedItemSellPrice(ItemPrefab item, PriceInfo priceInfo = null, bool considerRequestedGoods = true)
{
priceInfo ??= item?.GetPriceInfo(this);
if (priceInfo == null) { return 0; }
var price = (int)(StoreSellPriceModifier * priceInfo.Price);
float price = StoreSellPriceModifier * priceInfo.Price;
// Adjust by random price modifier
price = ((100 - StorePriceModifier) / 100.0f) * price;
// Adjust by current store balance
price = ActiveStoreBalanceStatus.SellPriceModifier * price;
// Adjust by requested good status
if (considerRequestedGoods && RequestedGoods.Contains(item))
{
price = RequestGoodPriceModifier * price;
}
// Adjust by current location reputation
if (Reputation.Value > 0.0f)
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f + StoreMaxReputationModifier, Reputation.Value / Reputation.MaxReputation) * price;
}
else
{
price = (int)(MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price);
price = MathHelper.Lerp(1.0f, 1.0f - StoreMaxReputationModifier, Reputation.Value / Reputation.MinReputation) * price;
}
// Item price should never go below 1 mk
return Math.Max(price, 1);
}
/// <summary>
/// If item.GetPriceInfo() returns null, this will return 0
/// </summary>
public int GetAdjustedItemSellPrice(ItemPrefab item) => GetAdjustedItemSellPrice(item?.GetPriceInfo(this));
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}
public int GetAdjustedMechanicalCost(int cost)
{
@@ -575,12 +818,12 @@ namespace Barotrauma
return (int) Math.Ceiling((1.0f - discount) * cost * MechanicalPriceMultiplier);
}
/// <summary>
/// If 'force' is true, the stock will be recreated even if it has been created previously already.
/// This is used when (at least) when the type of the location changes.
/// </summary>
/// <param name="force">If true, the store will be recreated if it already exists.</param>
public void CreateStore(bool force = false)
{
// In multiplayer, stores should be created by the server and loaded from save data by clients
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (!force && StoreStock != null) { return; }
if (StoreStock != null)
@@ -604,10 +847,16 @@ namespace Barotrauma
StoreCurrentBalance = StoreInitialBalance;
StoreStock = CreateStoreStock();
}
GenerateRandomPriceModifier();
CreateStoreSpecials();
}
public void UpdateStore()
{
// In multiplayer, stores should be updated by the server and loaded from save data by clients
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (StoreStock == null)
{
CreateStore();
@@ -619,6 +868,8 @@ namespace Barotrauma
StoreCurrentBalance = Math.Min(StoreCurrentBalance + (int)(StoreInitialBalance / 10.0f), StoreInitialBalance);
}
GenerateRandomPriceModifier();
var stock = StoreStock;
var stockToRemove = new List<PurchasedItem>();
foreach (PurchasedItem item in stock)
@@ -642,6 +893,56 @@ namespace Barotrauma
}
stockToRemove.ForEach(i => stock.Remove(i));
StoreStock = stock;
if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval)
{
CreateStoreSpecials();
}
}
private void GenerateRandomPriceModifier()
{
StorePriceModifier = Rand.Range(-StorePriceModifierRange, StorePriceModifierRange);
}
private void CreateStoreSpecials()
{
DailySpecials.Clear();
var availableStock = new Dictionary<ItemPrefab, float>();
foreach (var stockItem in StoreStock)
{
if (stockItem.Quantity < 1) { continue; }
var weight = 1.0f;
var priceInfo = stockItem.ItemPrefab.GetPriceInfo(this);
if (priceInfo != null)
{
if (!priceInfo.CanBeSpecial) { continue; }
var baseQuantity = priceInfo.MinAvailableAmount > 0 ? priceInfo.MinAvailableAmount : PriceInfo.DefaultAmount;
weight += (float)(stockItem.Quantity - baseQuantity) / baseQuantity;
if (weight < 0.0f) { continue; }
}
availableStock.Add(stockItem.ItemPrefab, weight);
}
for (int i = 0; i < DailySpecialsCount; i++)
{
if (availableStock.None()) { break; }
var item = ToolBox.SelectWeightedRandom(availableStock.Keys.ToList(), availableStock.Values.ToList(), Rand.RandSync.Unsynced);
if (item == null) { break; }
DailySpecials.Add(item);
availableStock.Remove(item);
}
RequestedGoods.Clear();
for (int i = 0; i < RequestedGoodsCount; i++)
{
var item = ItemPrefab.Prefabs.GetRandom(p =>
p.CanBeSold && !RequestedGoods.Contains(p) &&
p.GetPriceInfo(this) is PriceInfo pi && pi.CanBeSpecial);
if (item == null) { break; }
RequestedGoods.Add(item);
}
StepsSinceSpecialsUpdated = 0;
}
public void AddToStock(List<SoldItem> items)
@@ -686,6 +987,20 @@ namespace Barotrauma
}
}
public static StoreBalanceStatus GetStoreBalanceStatus(int balance)
{
StoreBalanceStatus nextStatus = DefaultBalanceStatus;
foreach (var balanceStatus in StoreBalanceStatuses)
{
if (balanceStatus.PercentageOfInitialBalance < nextStatus.PercentageOfInitialBalance &&
((float)balance / StoreInitialBalance) < balanceStatus.PercentageOfInitialBalance)
{
nextStatus = balanceStatus;
}
}
return nextStatus;
}
public XElement Save(Map map, XElement parentElement)
{
var locationElement = new XElement("location",
@@ -695,17 +1010,42 @@ namespace Barotrauma
new XAttribute("discovered", Discovered),
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
new XAttribute("pricemultiplier", PriceMultiplier),
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier));
if (ProximityTime.Length > 0 && ProximityTime.Any(t => t > 0))
{
locationElement.Add(new XAttribute("proximitytime", string.Join(',', ProximityTime.Select(i => i.ToString()))));
}
new XAttribute("isgatebetweenbiomes", IsGateBetweenBiomes),
new XAttribute("mechanicalpricemultipler", MechanicalPriceMultiplier),
new XAttribute("timesincelasttypechange", TimeSinceLastTypeChange),
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation));
LevelData.Save(locationElement);
if (TypeChangeTimer > 0)
for (int i = 0; i < Type.CanChangeTo.Count; i++)
{
locationElement.Add(new XAttribute("changetimer", TypeChangeTimer));
for (int j = 0; j < Type.CanChangeTo[i].Requirements.Count; j++)
{
if (ProximityTimer.ContainsKey(Type.CanChangeTo[i].Requirements[j]))
{
locationElement.Add(new XAttribute("proximitytimer" + i + "-" + j, ProximityTimer[Type.CanChangeTo[i].Requirements[j]]));
}
}
}
if (PendingLocationTypeChange.HasValue)
{
var changeElement = new XElement("pendinglocationtypechange", new XAttribute("timer", PendingLocationTypeChange.Value.delay));
if (PendingLocationTypeChange.Value.parentMission != null)
{
changeElement.Add(new XAttribute("missionidentifier", PendingLocationTypeChange.Value.parentMission.Identifier));
}
else
{
changeElement.Add(new XAttribute("index", Type.CanChangeTo.IndexOf(PendingLocationTypeChange.Value.typeChange)));
}
locationElement.Add(changeElement);
}
if (LocationTypeChangeCooldown > 0)
{
locationElement.Add(new XAttribute("locationtypechangecooldown", LocationTypeChangeCooldown));
}
if (takenItems.Any())
{
locationElement.Add(new XAttribute(
@@ -719,7 +1059,11 @@ namespace Barotrauma
if (StoreStock != null)
{
var storeElement = new XElement("store", new XAttribute("balance", StoreCurrentBalance));
var storeElement = new XElement("store",
new XAttribute("balance", StoreCurrentBalance),
new XAttribute("pricemodifier", StorePriceModifier),
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
foreach (PurchasedItem item in StoreStock)
{
if (item?.ItemPrefab == null) { continue; }
@@ -727,6 +1071,29 @@ namespace Barotrauma
new XAttribute("id", item.ItemPrefab.Identifier),
new XAttribute("qty", item.Quantity)));
}
if (DailySpecials.Any())
{
var dailySpecialElement = new XElement("dailyspecials");
foreach (var item in DailySpecials)
{
dailySpecialElement.Add(new XElement("item",
new XAttribute("id", item.Identifier)));
}
storeElement.Add(dailySpecialElement);
}
if (RequestedGoods.Any())
{
var requestedGoodsElement = new XElement("requestedgoods");
foreach (var item in RequestedGoods)
{
requestedGoodsElement.Add(new XElement("item",
new XAttribute("id", item.Identifier)));
}
storeElement.Add(requestedGoodsElement);
}
locationElement.Add(storeElement);
}
@@ -735,7 +1102,7 @@ namespace Barotrauma
var missionsElement = new XElement("missions");
foreach (Mission mission in missions)
{
var location = mission.Locations.FirstOrDefault(l => l != this);
var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
var i = map.Locations.IndexOf(location);
missionsElement.Add(new XElement("mission",
new XAttribute("prefabid", mission.Prefab.Identifier),
@@ -750,40 +1117,6 @@ namespace Barotrauma
return locationElement;
}
public int Distance(Location other, int maxRecursionDepth, int currRecursionDepth = 0)
{
if (currRecursionDepth >= maxRecursionDepth) { return -1; }
if (other == this) { return 0; }
int minDist = -1;
foreach (Location connected in Connections.Select(c => c.Locations.First(l => l != this)))
{
int dist = connected.Distance(other, maxRecursionDepth, currRecursionDepth+1);
if (dist >= 0)
{
if (minDist < 0 || dist < minDist) { minDist = dist; }
}
}
return minDist;
}
public void DetermineProximityTime(Location currentLocation)
{
int dist = Distance(currentLocation, Type.CanChangeTo.Select(cct => cct.RequiredProximityForProbabilityIncrease).Max());
for (int i=0;i<ProximityTime.Length;i++)
{
if (dist <= Type.CanChangeTo[i].RequiredProximityForProbabilityIncrease)
{
ProximityTime[i]++;
if (ProximityTime[i] > 5) { ProximityTime[i] = 5; }
}
else
{
ProximityTime[i]--;
if (ProximityTime[i] < 0) { ProximityTime[i] = 0; }
}
}
}
public void Remove()
{
RemoveProjSpecific();
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
@@ -31,8 +32,31 @@ namespace Barotrauma
private set;
}
private readonly List<Mission> availableMissions = new List<Mission>();
public IEnumerable<Mission> AvailableMissions
{
get
{
availableMissions.RemoveAll(m => m.Completed || (m.Failed && m.Prefab.AllowRetry));
return availableMissions;
}
}
public LocationConnection(Location location1, Location location2)
{
if (location1 == null)
{
throw new ArgumentException("Invalid location connection: location1 was null");
}
if (location2 == null)
{
throw new ArgumentException("Invalid location connection: location2 was null");
}
if (location1 == location2)
{
throw new ArgumentException("Invalid location connection: location1 was the same as location2");
}
Locations = new Location[] { location1, location2 };
Length = Vector2.Distance(location1.MapPosition, location2.MapPosition);
}
@@ -13,17 +13,12 @@ namespace Barotrauma
class LocationType
{
public static readonly List<LocationType> List = new List<LocationType>();
private readonly List<string> nameFormats;
private readonly List<string> names;
private readonly Sprite symbolSprite;
private readonly List<Sprite> portraits = new List<Sprite>();
//<name, commonness>
private List<Tuple<JobPrefab, float>> hireableJobs;
private float totalHireableWeight;
private readonly List<Tuple<JobPrefab, float>> hireableJobs;
private readonly float totalHireableWeight;
public Dictionary<int, float> CommonnessPerZone = new Dictionary<int, float>();
@@ -34,16 +29,20 @@ namespace Barotrauma
public readonly List<LocationTypeChange> CanChangeTo = new List<LocationTypeChange>();
public readonly List<string> MissionIdentifiers = new List<string>();
public readonly List<string> MissionTags = new List<string>();
public readonly List<string> HideEntitySubcategories = new List<string>();
public bool IsEnterable { get; private set; }
public bool UseInMainMenu
{
get;
private set;
}
public List<string> NameFormats
{
get { return nameFormats; }
}
public List<string> NameFormats { get; private set; }
public bool HasHireableCharacters
{
@@ -56,10 +55,7 @@ namespace Barotrauma
private set;
}
public Sprite Sprite
{
get { return symbolSprite; }
}
public Sprite Sprite { get; private set; }
public Color SpriteColor
{
@@ -79,9 +75,15 @@ namespace Barotrauma
BeaconStationChance = element.GetAttributeFloat("beaconstationchance", 0.0f);
nameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
NameFormats = TextManager.GetAll("LocationNameFormat." + Identifier);
UseInMainMenu = element.GetAttributeBool("useinmainmenu", false);
HasOutpost = element.GetAttributeBool("hasoutpost", true);
IsEnterable = element.GetAttributeBool("isenterable", HasOutpost);
MissionIdentifiers = element.GetAttributeStringArray("missionidentifiers", new string[0]).ToList();
MissionTags = element.GetAttributeStringArray("missiontags", new string[0]).ToList();
HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", new string[0]).ToList();
string nameFile = element.GetAttributeString("namefile", "Content/Map/locationNames.txt");
try
@@ -135,11 +137,11 @@ namespace Barotrauma
hireableJobs.Add(hireableJob);
break;
case "symbol":
symbolSprite = new Sprite(subElement, lazyLoad: true);
Sprite = new Sprite(subElement, lazyLoad: true);
SpriteColor = subElement.GetAttributeColor("color", Color.White);
break;
case "changeto":
CanChangeTo.Add(new LocationTypeChange(Identifier, subElement));
CanChangeTo.Add(new LocationTypeChange(Identifier, subElement, requireChangeMessages: true));
break;
case "portrait":
var portrait = new Sprite(subElement, lazyLoad: true);
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -6,41 +8,268 @@ namespace Barotrauma
{
class LocationTypeChange
{
public class Requirement
{
public enum FunctionType
{
Add,
Multiply
}
public readonly FunctionType Function;
/// <summary>
/// The change can only happen if there's at least one of the given types of locations near this one
/// </summary>
public readonly List<string> RequiredLocations;
/// <summary>
/// How close the location needs to be to one of the RequiredLocations for the change to occur
/// </summary>
public readonly int RequiredProximity;
/// <summary>
/// Base probability per turn for the location to change if near one of the RequiredLocations
/// </summary>
public readonly float Probability;
/// <summary>
/// How close the location needs to be to one of the RequiredLocations for the probability to increase
/// </summary>
public readonly int RequiredProximityForProbabilityIncrease;
/// <summary>
/// How much the probability increases per turn if within RequiredProximityForProbabilityIncrease steps of RequiredLocations
/// </summary>
public readonly float ProximityProbabilityIncrease;
/// <summary>
/// Does there need to be a beacon station within RequiredProximity
/// </summary>
public readonly bool RequireBeaconStation;
/// <summary>
/// Does there need to be hunting grounds within RequiredProximity
/// </summary>
public readonly bool RequireHuntingGrounds;
public Requirement(XElement element, LocationTypeChange change)
{
RequiredLocations = element.GetAttributeStringArray("requiredlocations", element.GetAttributeStringArray("requiredadjacentlocations", new string[0])).ToList();
RequiredProximity = Math.Max(element.GetAttributeInt("requiredproximity", 1), 1);
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", -1);
RequireBeaconStation = element.GetAttributeBool("requirebeaconstation", false);
RequireHuntingGrounds = element.GetAttributeBool("requirehuntinggrounds", false);
string functionStr = element.GetAttributeString("function", "Add");
if (!Enum.TryParse(functionStr, ignoreCase: true, out Function))
{
DebugConsole.ThrowError(
$"Invalid location type change in location type \"{change.CurrentType}\". " +
$"\"{functionStr}\" is not a valid function.");
}
Probability = element.GetAttributeFloat("probability", 1.0f);
if (RequiredProximityForProbabilityIncrease > 0 || ProximityProbabilityIncrease > 0.0f)
{
if (!RequiredLocations.Any() && !RequireBeaconStation && !RequireHuntingGrounds)
{
DebugConsole.AddWarning(
$"Invalid location type change in location type \"{change.CurrentType}\". " +
"Probability is configured to increase when near some other type of location, but the RequiredLocations attribute is not set.");
}
if (Probability >= 1.0f)
{
DebugConsole.AddWarning(
$"Invalid location type change in location type \"{change.CurrentType}\". " +
"Probability is configured to increase when near some other type of location, but the base probability is already 100%");
}
}
}
public bool MatchesLocation(Location location)
{
return RequiredLocations.Contains(location.Type.Identifier) && !location.IsCriticallyRadiated();
}
public bool AnyWithinDistance(Location location, int maxDistance, int currentDistance = 0, HashSet<Location> checkedLocations = null)
{
if (currentDistance > maxDistance) { return false; }
if (currentDistance > 0 && MatchesLocation(location)) { return true; }
checkedLocations ??= new HashSet<Location>();
checkedLocations.Add(location);
foreach (var connection in location.Connections)
{
if (RequireBeaconStation && connection.LevelData.HasBeaconStation && connection.LevelData.IsBeaconActive)
{
return true;
}
if (RequireHuntingGrounds && connection.LevelData.HasHuntingGrounds)
{
return true;
}
var otherLocation = connection.OtherLocation(location);
if (!checkedLocations.Contains(otherLocation))
{
if (AnyWithinDistance(otherLocation, maxDistance, currentDistance + 1, checkedLocations)) { return true; }
}
}
return false;
}
}
public readonly string CurrentType;
public readonly string ChangeToType;
/// <summary>
/// Base probability per turn for the location to change if near one of the RequiredLocations
/// </summary>
public readonly float Probability;
public readonly int RequiredDuration;
public readonly float ProximityProbabilityIncrease;
public readonly int RequiredProximityForProbabilityIncrease;
public readonly bool RequireDiscovered;
public List<Requirement> Requirements = new List<Requirement>();
public List<string> Messages = new List<string>();
//the change can't happen if there's a location of the given type next to this one
/// <summary>
/// The change can't happen if there's one or more of the given types of locations near this one
/// </summary>
public readonly List<string> DisallowedAdjacentLocations;
//the change can only happen if there's at least one of the given types of locations next to this one
public readonly List<string> RequiredAdjacentLocations;
/// <summary>
/// How close the location needs to be to one of the DisallowedAdjacentLocations for the change to be disabled
/// </summary>
public readonly int DisallowedProximity;
public LocationTypeChange(string currentType, XElement element)
/// <summary>
/// The location can't change it's type for this many turns after this location type changes occurs
/// </summary>
public readonly int CooldownAfterChange;
public readonly Point RequiredDurationRange;
public LocationTypeChange(string currentType, XElement element, bool requireChangeMessages, float defaultProbability = 0.0f)
{
ChangeToType = element.GetAttributeString("type", "");
Probability = element.GetAttributeFloat("probability", 1.0f);
RequiredDuration = element.GetAttributeInt("requiredduration", 0);
CurrentType = currentType;
ChangeToType = element.GetAttributeString("type", element.GetAttributeString("to", ""));
ProximityProbabilityIncrease = element.GetAttributeFloat("proximityprobabilityincrease", 0.0f);
RequiredProximityForProbabilityIncrease = element.GetAttributeInt("requiredproximityforprobabilityincrease", 0);
RequireDiscovered = element.GetAttributeBool("requirediscovered", false);
DisallowedAdjacentLocations = element.GetAttributeStringArray("disallowedadjacentlocations", new string[0]).ToList();
RequiredAdjacentLocations = element.GetAttributeStringArray("requiredadjacentlocations", new string[0]).ToList();
DisallowedProximity = Math.Max(element.GetAttributeInt("disallowedproximity", 1), 1);
RequiredDurationRange = element.GetAttributePoint("requireddurationrange", Point.Zero);
Probability = element.GetAttributeFloat("probability", defaultProbability);
CooldownAfterChange = Math.Max(element.GetAttributeInt("cooldownafterchange", 0), 0);
//backwards compatibility
if (element.Attribute("requiredlocations") != null)
{
Requirements.Add(new Requirement(element, this));
}
//backwards compatibility
if (element.Attribute("requiredduration") != null)
{
RequiredDurationRange = new Point(element.GetAttributeInt("requiredduration", 0));
}
string messageTag = element.GetAttributeString("messagetag", "LocationChange." + currentType + ".ChangeTo." + ChangeToType);
Messages = TextManager.GetAll(messageTag);
if (Messages == null)
{
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
if (requireChangeMessages)
{
DebugConsole.ThrowError("No messages defined for the location type change " + currentType + " -> " + ChangeToType);
}
Messages = new List<string>();
}
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("requirement", StringComparison.OrdinalIgnoreCase))
{
Requirements.Add(new Requirement(subElement, this));
}
}
}
public float DetermineProbability(Location location)
{
if (RequireDiscovered && !location.Discovered) { return 0.0f; }
if (location.IsCriticallyRadiated()) { return 0.0f; }
if (location.LocationTypeChangeCooldown > 0) { return 0.0f; }
if (location.IsGateBetweenBiomes) { return 0.0f; }
if (DisallowedAdjacentLocations.Any() &&
AnyWithinDistance(location, DisallowedProximity, (otherLocation) => { return DisallowedAdjacentLocations.Contains(otherLocation.Type.Identifier); }))
{
return 0.0f;
}
float probability = Probability;
foreach (Requirement requirement in Requirements)
{
if (requirement.AnyWithinDistance(location, requirement.RequiredProximity))
{
if (requirement.Function == Requirement.FunctionType.Add)
{
probability += requirement.Probability;
}
else
{
probability *= requirement.Probability;
}
}
if (location.ProximityTimer.ContainsKey(requirement))
{
if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
{
if (requirement.Function == Requirement.FunctionType.Add)
{
probability += requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement];
}
else
{
probability *= requirement.ProximityProbabilityIncrease * location.ProximityTimer[requirement];
}
}
}
}
return probability;
}
private bool AnyWithinDistance(Location location, int maxDistance, Func<Location, bool> predicate, int currentDistance = 0, HashSet<Location> checkedLocations = null)
{
if (currentDistance > maxDistance) { return false; }
if (currentDistance > 0 && predicate(location)) { return true; }
checkedLocations ??= new HashSet<Location>();
checkedLocations.Add(location);
foreach (var connection in location.Connections)
{
var otherLocation = connection.OtherLocation(location);
if (!checkedLocations.Contains(otherLocation))
{
if (AnyWithinDistance(otherLocation, maxDistance, predicate, currentDistance + 1, checkedLocations)) { return true; }
}
}
return false;
}
}
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,8 +16,8 @@ namespace Barotrauma
private Location furthestDiscoveredLocation;
private int Width => generationParams.Width;
private int Height => generationParams.Height;
public int Width => generationParams.Width;
public int Height => generationParams.Height;
public Action<Location, LocationConnection> OnLocationSelected;
/// <summary>
@@ -56,11 +57,14 @@ namespace Barotrauma
public List<LocationConnection> Connections { get; private set; }
public Radiation Radiation;
public Map()
{
generationParams = MapGenerationParams.Instance;
Locations = new List<Location>();
Connections = new List<LocationConnection>();
Radiation = new Radiation(this, generationParams.RadiationParams);
}
/// <summary>
@@ -69,6 +73,7 @@ namespace Barotrauma
private Map(CampaignMode campaign, XElement element) : this()
{
Seed = element.GetAttributeString("seed", "a");
Rand.SetSyncedSeed(ToolBox.StringToInt(Seed));
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -80,11 +85,17 @@ namespace Barotrauma
Locations.Add(null);
}
Locations[i] = new Location(subElement);
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
break;
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
break;
}
}
System.Diagnostics.Debug.Assert(!Locations.Contains(null));
for (int i = 0; i < Locations.Count; i++)
{
Locations[i].Reputation ??= new Reputation(campaign.CampaignMetadata, $"location.{i}", -100, 100, Rand.Range(-10, 10, Rand.RandSync.Server));
}
foreach (XElement subElement in element.Elements())
{
@@ -92,6 +103,7 @@ namespace Barotrauma
{
case "connection":
Point locationIndices = subElement.GetAttributePoint("locations", new Point(0, 1));
if (locationIndices.X == locationIndices.Y) { continue; }
var connection = new LocationConnection(Locations[locationIndices.X], Locations[locationIndices.Y])
{
Passed = subElement.GetAttributeBool("passed", false),
@@ -181,8 +193,8 @@ namespace Barotrauma
}
System.Diagnostics.Debug.Assert(StartLocation != null, "Start location not assigned after level generation.");
CurrentLocation.CreateStore();
CurrentLocation.Discovered = true;
CurrentLocation.CreateStore();
InitProjectSpecific();
}
@@ -243,21 +255,21 @@ namespace Barotrauma
for (int i = 0; i < 2; i++)
{
if (newLocations[i] != null) continue;
if (newLocations[i] != null) { continue; }
Vector2[] points = new Vector2[] { edge.Point1, edge.Point2 };
int positionIndex = Rand.Int(1, Rand.RandSync.Server);
Vector2 position = points[positionIndex];
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) position = points[1 - positionIndex];
int zone = MathHelper.Clamp((int)Math.Floor(position.X / zoneWidth) + 1, 1, generationParams.DifficultyZones);
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, Locations);
if (newLocations[1 - i] != null && newLocations[1 - i].MapPosition == position) { position = points[1 - positionIndex]; }
int zone = GetZoneIndex(position.X);
newLocations[i] = Location.CreateRandom(position, zone, Rand.GetRNG(Rand.RandSync.Server), requireOutpost: false, existingLocations: Locations);
Locations.Add(newLocations[i]);
}
var newConnection = new LocationConnection(newLocations[0], newLocations[1]);
Connections.Add(newConnection);
Connections.Add(newConnection);
}
//remove connections that are too short
@@ -316,7 +328,15 @@ namespace Barotrauma
{
connection.Locations[1] = Locations[i];
}
Locations[i].Connections.Add(connection);
if (connection.Locations[0] != connection.Locations[1])
{
Locations[i].Connections.Add(connection);
}
else
{
Connections.Remove(connection);
}
}
Locations[i].Connections.RemoveAll(c => c.OtherLocation(Locations[i]) == Locations[j]);
Locations.RemoveAt(j);
@@ -337,6 +357,56 @@ namespace Barotrauma
}
}
LocationConnection[] connectionsBetweenZones = new LocationConnection[generationParams.DifficultyZones];
foreach (var connection in Connections)
{
int zone1 = GetZoneIndex(connection.Locations[0].MapPosition.X);
int zone2 = GetZoneIndex(connection.Locations[1].MapPosition.X);
if (zone1 == zone2) { continue; }
if (zone1 > zone2)
{
int temp = zone2;
zone2 = zone1;
zone1 = temp;
}
if (connectionsBetweenZones[zone1] == null)
{
connectionsBetweenZones[zone1] = connection;
}
else
{
if (Math.Abs(connection.CenterPos.Y - Height / 2) < Math.Abs(connectionsBetweenZones[zone1].CenterPos.Y - Height / 2))
{
connectionsBetweenZones[zone1] = connection;
}
}
}
for (int i = Connections.Count - 1; i >= 0; i--)
{
int zone1 = GetZoneIndex(Connections[i].Locations[0].MapPosition.X);
int zone2 = GetZoneIndex(Connections[i].Locations[1].MapPosition.X);
if (zone1 == zone2) { continue; }
if (!connectionsBetweenZones.Contains(Connections[i]))
{
Connections.RemoveAt(i);
}
else
{
var leftMostLocation =
Connections[i].Locations[0].MapPosition.X < Connections[i].Locations[1].MapPosition.X ?
Connections[i].Locations[0] :
Connections[i].Locations[1];
if (!leftMostLocation.Type.HasOutpost)
{
leftMostLocation.ChangeType(LocationType.List.First(lt => lt.HasOutpost));
}
leftMostLocation.IsGateBetweenBiomes = true;
}
}
foreach (Location location in Locations)
{
for (int i = location.Connections.Count - 1; i >= 0; i--)
@@ -359,6 +429,14 @@ namespace Barotrauma
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location);
if (location.Type.MissionIdentifiers.Any())
{
location.UnlockMissionByIdentifier(location.Type.MissionIdentifiers.GetRandom());
}
if (location.Type.MissionTags.Any())
{
location.UnlockMissionByTag(location.Type.MissionTags.GetRandom());
}
}
foreach (LocationConnection connection in Connections)
{
@@ -368,6 +446,12 @@ namespace Barotrauma
partial void GenerateLocationConnectionVisuals();
private int GetZoneIndex(float xPos)
{
float zoneWidth = Width / generationParams.DifficultyZones;
return MathHelper.Clamp((int)Math.Floor(xPos / zoneWidth) + 1, 1, generationParams.DifficultyZones);
}
public Biome GetBiome(Vector2 mapPos)
{
return GetBiome(mapPos.X);
@@ -544,6 +628,12 @@ namespace Barotrauma
CurrentLocation.CreateStore();
OnLocationChanged?.Invoke(prevLocation, CurrentLocation);
if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.CampaignMetadata is { } metadata)
{
metadata.SetValue("campaign.location.id", CurrentLocationIndex);
metadata.SetValue("campaign.location.name", CurrentLocation.Name);
}
}
public void SetLocation(int index)
@@ -618,7 +708,6 @@ namespace Barotrauma
public void SelectMission(int missionIndex)
{
if (SelectedConnection == null) { return; }
if (CurrentLocation == null)
{
string errorMsg = "Failed to select a mission (current location not set).";
@@ -628,11 +717,18 @@ namespace Barotrauma
}
CurrentLocation.SelectedMissionIndex = missionIndex;
//the destination must be the same as the destination of the mission
if (CurrentLocation.SelectedMission != null &&
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
if (CurrentLocation.SelectedMission == null) { return; }
if (CurrentLocation.SelectedMission.Locations[0] != CurrentLocation ||
CurrentLocation.SelectedMission.Locations[1] != CurrentLocation)
{
CurrentLocation.SelectedMissionIndex = -1;
if (SelectedConnection == null) { return; }
//the destination must be the same as the destination of the mission
if (CurrentLocation.SelectedMission != null &&
CurrentLocation.SelectedMission.Locations[1] != SelectedLocation)
{
CurrentLocation.SelectedMissionIndex = -1;
}
}
OnMissionSelected?.Invoke(SelectedConnection, CurrentLocation.SelectedMission);
@@ -668,89 +764,116 @@ namespace Barotrauma
{
ProgressWorld();
}
Radiation.OnStep(steps);
}
private void ProgressWorld()
{
foreach (Location location in Locations)
{
if (!location.Discovered) { continue; }
if (furthestDiscoveredLocation == null || location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
if (location.Discovered)
{
furthestDiscoveredLocation = location;
if (furthestDiscoveredLocation == null ||
location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
furthestDiscoveredLocation = location;
}
}
}
foreach (Location location in Locations)
{
if (location.MapPosition.X > furthestDiscoveredLocation.MapPosition.X)
{
continue;
}
if (location == CurrentLocation || location == SelectedLocation) { continue; }
//find which types of locations this one can change to
var cct = location.Type.CanChangeTo;
List<LocationTypeChange> allowedTypeChanges = new List<LocationTypeChange>();
List<int> readyTypeChanges = new List<int>();
for (int i = 0; i < cct.Count; i++)
ProgressLocationTypeChanges(location);
if (location.Discovered)
{
LocationTypeChange typeChange = cct[i];
//check if there are any adjacent locations that would prevent the change
bool disallowedFound = false;
foreach (string disallowedLocationName in typeChange.DisallowedAdjacentLocations)
{
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(disallowedLocationName, StringComparison.OrdinalIgnoreCase)))
{
disallowedFound = true;
break;
}
}
if (disallowedFound) { continue; }
//check that there's a required adjacent location present
bool requiredFound = false;
foreach (string requiredLocationName in typeChange.RequiredAdjacentLocations)
{
if (location.Connections.Any(c => c.OtherLocation(location).Type.Identifier.Equals(requiredLocationName, StringComparison.OrdinalIgnoreCase)))
{
requiredFound = true;
break;
}
}
if (!requiredFound && typeChange.RequiredAdjacentLocations.Count > 0) { continue; }
allowedTypeChanges.Add(typeChange);
if (location.TypeChangeTimer >= typeChange.RequiredDuration)
{
readyTypeChanges.Add(i);
}
location.UpdateStore();
}
}
}
//select a random type change
if (Rand.Range(0.0f, 1.0f) < readyTypeChanges.Sum(i => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[i])))
private void ProgressLocationTypeChanges(Location location)
{
location.TimeSinceLastTypeChange++;
location.LocationTypeChangeCooldown--;
if (location.PendingLocationTypeChange != null)
{
if (location.PendingLocationTypeChange.Value.typeChange.DetermineProbability(location) <= 0.0f)
{
var selectedTypeChangeIndex =
ToolBox.SelectWeightedRandom(
readyTypeChanges,
readyTypeChanges.Select(i => cct[i].Probability + (cct[i].ProximityProbabilityIncrease * (float)location.ProximityTime[i])).ToList(),
Rand.RandSync.Unsynced);
var selectedTypeChange = cct[selectedTypeChangeIndex];
if (selectedTypeChange != null)
{
string prevName = location.Name;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(selectedTypeChange.ChangeToType, StringComparison.OrdinalIgnoreCase)));
ChangeLocationType(location, prevName, selectedTypeChange);
location.TypeChangeTimer = -1;
break;
}
}
if (allowedTypeChanges.Count > 0)
{
location.TypeChangeTimer++;
//remove pending type change if it's no longer allowed
location.PendingLocationTypeChange = null;
}
else
{
location.TypeChangeTimer = 0;
location.PendingLocationTypeChange =
(location.PendingLocationTypeChange.Value.typeChange,
location.PendingLocationTypeChange.Value.delay - 1,
location.PendingLocationTypeChange.Value.parentMission);
if (location.PendingLocationTypeChange.Value.delay <= 0)
{
ChangeLocationType(location, location.PendingLocationTypeChange.Value.typeChange);
}
return;
}
}
location.UpdateStore();
//find which types of locations this one can change to
Dictionary<LocationTypeChange, float> allowedTypeChanges = new Dictionary<LocationTypeChange, float>();
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
{
float probability = typeChange.DetermineProbability(location);
if (probability <= 0.0f) { continue; }
allowedTypeChanges.Add(typeChange, probability);
}
//select a random type change
if (Rand.Range(0.0f, 1.0f) < allowedTypeChanges.Sum(change => change.Value))
{
var selectedTypeChange =
ToolBox.SelectWeightedRandom(
allowedTypeChanges.Keys.ToList(),
allowedTypeChanges.Values.ToList(),
Rand.RandSync.Unsynced);
if (selectedTypeChange != null)
{
if (selectedTypeChange.RequiredDurationRange.X > 0)
{
location.PendingLocationTypeChange =
(selectedTypeChange,
Rand.Range(selectedTypeChange.RequiredDurationRange.X, selectedTypeChange.RequiredDurationRange.Y),
null);
}
else
{
ChangeLocationType(location, selectedTypeChange);
}
return;
}
}
foreach (LocationTypeChange typeChange in location.Type.CanChangeTo)
{
foreach (var requirement in typeChange.Requirements)
{
if (requirement.AnyWithinDistance(location, requirement.RequiredProximityForProbabilityIncrease))
{
if (!location.ProximityTimer.ContainsKey(requirement)) { location.ProximityTimer[requirement] = 0; }
location.ProximityTimer[requirement] += 1;
}
else
{
location.ProximityTimer.Remove(requirement);
}
}
}
}
@@ -799,7 +922,22 @@ namespace Barotrauma
return distance;
}
partial void ChangeLocationType(Location location, string prevName, LocationTypeChange change);
private void ChangeLocationType(Location location, LocationTypeChange change)
{
string prevName = location.Name;
location.ChangeType(LocationType.List.Find(lt => lt.Identifier.Equals(change.ChangeToType, StringComparison.OrdinalIgnoreCase)));
ChangeLocationTypeProjSpecific(location, prevName, change);
foreach (var requirement in change.Requirements)
{
location.ProximityTimer.Remove(requirement);
}
location.TimeSinceLastTypeChange = 0;
location.LocationTypeChangeCooldown = change.CooldownAfterChange;
location.PendingLocationTypeChange = null;
}
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
partial void ClearAnimQueue();
/// <summary>
@@ -835,8 +973,15 @@ namespace Barotrauma
{
case "location":
Location location = Locations[subElement.GetAttributeInt("i", 0)];
location.TypeChangeTimer = subElement.GetAttributeInt("changetimer", 0);
location.ProximityTimer.Clear();
for (int i = 0; i < location.Type.CanChangeTo.Count; i++)
{
for (int j = 0; j < location.Type.CanChangeTo[i].Requirements.Count; j++)
{
location.ProximityTimer.Add(location.Type.CanChangeTo[i].Requirements[j], subElement.GetAttributeInt("changetimer" + i + "-" + j, 0));
}
}
location.LoadLocationTypeChange(subElement);
location.Discovered = subElement.GetAttributeBool("discovered", false);
if (location.Discovered)
{
@@ -849,7 +994,6 @@ namespace Barotrauma
}
}
string locationType = subElement.GetAttributeString("type", "");
string prevLocationName = location.Name;
LocationType prevLocationType = location.Type;
@@ -860,15 +1004,22 @@ namespace Barotrauma
var change = prevLocationType.CanChangeTo.Find(c => c.ChangeToType.Equals(location.Type.Identifier, StringComparison.OrdinalIgnoreCase));
if (change != null)
{
ChangeLocationType(location, prevLocationName, change);
ChangeLocationTypeProjSpecific(location, prevLocationName, change);
location.TimeSinceLastTypeChange = 0;
}
}
location.LoadStore(subElement);
location.LoadMissions(subElement);
break;
case "connection":
int connectionIndex = subElement.GetAttributeInt("i", 0);
Connections[connectionIndex].Passed = subElement.GetAttributeBool("passed", false);
break;
case "radiation":
Radiation = new Radiation(this, generationParams.RadiationParams, subElement);
break;
}
}
@@ -935,6 +1086,8 @@ namespace Barotrauma
mapElement.Add(connectionElement);
}
mapElement.Add(Radiation.Save());
element.Add(mapElement);
}
@@ -125,6 +125,8 @@ namespace Barotrauma
get; private set;
}
public RadiationParams RadiationParams;
public static void Init()
{
@@ -238,6 +240,9 @@ namespace Barotrauma
TypeChangeIcon = new Sprite(subElement);
break;
#endif
case "radiationparams":
RadiationParams = new RadiationParams(subElement);
break;
}
}
}
@@ -0,0 +1,140 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal partial class Radiation : ISerializableEntity
{
public string Name => nameof(Radiation);
[Serialize(defaultValue: 0f, isSaveable: true)]
public float Amount { get; set; }
public Dictionary<string, SerializableProperty> SerializableProperties { get; }
public readonly Map Map;
public readonly RadiationParams Params;
private float radiationTimer;
private float increasedAmount;
private float lastIncrease;
public bool Enabled = true;
public Radiation(Map map, RadiationParams radiationParams, XElement? element = null)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
Map = map;
Params = radiationParams;
radiationTimer = Params.RadiationDamageDelay;
if (element == null)
{
Amount = Params.StartingRadiation;
}
}
/// <summary>
/// Advances the progress of the radiation.
/// </summary>
/// <param name="steps"></param>
public void OnStep(float steps = 1)
{
if (!Enabled) { return; }
if (steps <= 0) { return; }
IncreaseRadiation(Params.RadiationStep * steps);
int amountOfOutposts = Map.Locations.Count(location => location.Type.HasOutpost && !location.IsCriticallyRadiated());
foreach (Location location in Map.Locations.Where(Contains))
{
if (amountOfOutposts <= Params.MinimumOutpostAmount) { break; }
if (Map.CurrentLocation is { } currLocation)
{
// Don't advance on nearby locations to avoid buggy behavior
if (currLocation == location || currLocation.Connections.Any(lc => lc.OtherLocation(currLocation) == location)) { continue; }
}
bool wasCritical = location.IsCriticallyRadiated();
location.TurnsInRadiation++;
if (location.Type.HasOutpost && !wasCritical && location.IsCriticallyRadiated())
{
amountOfOutposts--;
}
}
}
public void IncreaseRadiation(float amount)
{
Amount += amount;
increasedAmount = lastIncrease = amount;
}
public void UpdateRadiation(float deltaTime)
{
if (!(GameMain.GameSession?.IsCurrentLocationRadiated() ?? false)) { return; }
if (GameMain.NetworkMember is { IsClient: true }) { return; }
if (radiationTimer > 0)
{
radiationTimer -= deltaTime;
return;
}
radiationTimer = Params.RadiationDamageDelay;
foreach (Character character in Character.CharacterList)
{
if (character.IsDead || character.Removed || !(character.CharacterHealth is { } health)) { continue; }
if (IsEntityRadiated(character))
{
health.ApplyAffliction(null, new Affliction(AfflictionPrefab.RadiationSickness, Params.RadiationDamageAmount));
}
}
}
public bool Contains(Location location)
{
return Contains(location.MapPosition);
}
public bool Contains(Vector2 pos)
{
return pos.X < Amount;
}
public bool IsEntityRadiated(Entity entity)
{
if (!Enabled) { return false; }
if (Level.Loaded is { Type: LevelData.LevelType.LocationConnection, StartLocation: { } startLocation, EndLocation: { } endLocation } level)
{
if (Contains(startLocation) && Contains(endLocation)) { return true; }
float distance = MathHelper.Clamp((entity.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X), 0.0f, 1.0f);
var (startX, startY) = startLocation.MapPosition;
var (endX, endY) = endLocation.MapPosition;
Vector2 mapPos = new Vector2(startX + (endX - startX), startY + (endY - startY)) * distance;
return Contains(mapPos);
}
return false;
}
public XElement Save()
{
XElement element = new XElement(nameof(Radiation));
SerializableProperty.SerializeProperties(this, element);
return element;
}
}
}
@@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
internal class RadiationParams: ISerializableEntity
{
public string Name => nameof(RadiationParams);
public Dictionary<string, SerializableProperty> SerializableProperties { get; }
[Serialize(defaultValue: -100f, isSaveable: false, "How much radiation the world starts with.")]
public float StartingRadiation { get; set; }
[Serialize(defaultValue: 100f, isSaveable: false, "How much radiation is added on each step.")]
public float RadiationStep { get; set; }
[Serialize(defaultValue: 10, isSaveable: false, "How many turns in radiation does it take for an outpost to be removed from the map.")]
public int CriticalRadiationThreshold { get; set; }
[Serialize(defaultValue: 3, isSaveable: false, "Minimum amount of outposts in the level that cannot be removed due to radiation.")]
public int MinimumOutpostAmount { get; set; }
[Serialize(defaultValue: 3f, isSaveable: false, "How fast the radiation increase animation goes.")]
public float AnimationSpeed { get; set; }
[Serialize(defaultValue: 10f, isSaveable: false, "How long it takes to apply more radiation damage while in a radiated zone.")]
public float RadiationDamageDelay { get; set; }
[Serialize(defaultValue: 1f, isSaveable: false, "How much is the radiation affliction increased by while in a radiated zone.")]
public float RadiationDamageAmount { get; set; }
public RadiationParams(XElement element)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
}
}
@@ -249,10 +249,6 @@ namespace Barotrauma
{
get { return ""; }
}
private bool ignoreByAI;
public bool IgnoreByAI => ignoreByAI;
public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore;
public MapEntity(MapEntityPrefab prefab, Submarine submarine, ushort id) : base(submarine, id)
{
@@ -624,6 +620,21 @@ namespace Barotrauma
continue;
}
if (t == typeof(Structure))
{
string name = element.Attribute("name").Value;
string identifier = element.GetAttributeString("identifier", "");
StructurePrefab structurePrefab = Structure.FindPrefab(name, identifier);
if (structurePrefab == null)
{
ItemPrefab itemPrefab = ItemPrefab.Find(name, identifier);
if (itemPrefab != null)
{
t = typeof(Item);
}
}
}
try
{
MethodInfo loadMethod = t.GetMethod("Load", new[] { typeof(XElement), typeof(Submarine), typeof(IdRemap) });
@@ -3,14 +3,23 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
[Flags]
enum MapEntityCategory
{
Structure = 1, Decorative = 2, Machine = 4, Equipment = 8, Electrical = 16, Material = 32, Misc = 64, Alien = 128, Wrecked = 256, Thalamus = 512, ItemAssembly = 1024, Legacy = 2048
Structure = 1,
Decorative = 2,
Machine = 4,
Equipment = 8,
Electrical = 16,
Material = 32,
Misc = 64,
Alien = 128,
Wrecked = 256,
ItemAssembly = 512,
Legacy = 1024
}
abstract partial class MapEntityPrefab : IPrefab, IDisposable
@@ -54,6 +63,7 @@ namespace Barotrauma
//is it possible to stretch the entity horizontally/vertically
[Serialize(false, false)]
public bool ResizeHorizontal { get; protected set; }
[Serialize(false, false)]
public bool ResizeVertical { get; protected set; }
@@ -118,6 +128,9 @@ namespace Barotrauma
[Serialize(false, false)]
public bool HideInMenus { get; set; }
[Serialize("", false)]
public string Subcategory { get; set; }
[Serialize(false, false)]
public bool Linkable
{
@@ -215,6 +228,11 @@ namespace Barotrauma
return string.IsNullOrWhiteSpace(AllowedUpgrades) ? new string[0] : AllowedUpgrades.Split(",");
}
public bool HasSubCategory(string subcategory)
{
return subcategory?.Equals(this.Subcategory, StringComparison.OrdinalIgnoreCase) ?? false;
}
protected virtual void CreateInstance(Rectangle rect)
{
if (constructor == null) return;
@@ -39,6 +39,34 @@ namespace Barotrauma
set;
}
[Serialize(false, isSaveable: true), Editable]
public bool AlwaysDestructible
{
get;
set;
}
[Serialize(false, isSaveable: true), Editable]
public bool AlwaysRewireable
{
get;
set;
}
[Serialize(false, isSaveable: true), Editable]
public bool AllowStealing
{
get;
set;
}
[Serialize(true, isSaveable: true), Editable]
public bool SpawnCrewInsideOutpost
{
get;
set;
}
private readonly Dictionary<string, int> moduleCounts = new Dictionary<string, int>();
public IEnumerable<KeyValuePair<string, int>> ModuleCounts
@@ -169,6 +169,7 @@ namespace Barotrauma
Type = SubmarineType.Outpost
};
generationFailed = false;
outpostInfo.OutpostGenerationParams = generationParams;
sub = new Submarine(outpostInfo, loadEntities: loadEntities);
sub.Info.OutpostGenerationParams = generationParams;
if (!generationFailed)
@@ -669,10 +670,15 @@ namespace Barotrauma
if (availableModules.Count() == 0) { return null; }
var modulesSuitableForLocationType =
availableModules.Where(m =>
!m.OutpostModuleInfo.AllowedLocationTypes.Any() ||
m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
{
@@ -705,10 +711,15 @@ namespace Barotrauma
if (availableModules.Count() == 0) { return null; }
//try to search for modules made specifically for this location type first
var modulesSuitableForLocationType =
availableModules.Where(m =>
!m.OutpostModuleInfo.AllowedLocationTypes.Any() ||
m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
availableModules.Where(m => m.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType.Identifier.ToLowerInvariant()));
//if not found, search for modules suitable for any location type
if (!modulesSuitableForLocationType.Any())
{
modulesSuitableForLocationType = availableModules.Where(m => !m.OutpostModuleInfo.AllowedLocationTypes.Any());
}
if (!modulesSuitableForLocationType.Any())
{
@@ -1381,15 +1392,14 @@ namespace Barotrauma
Rand.SetSyncedSeed(ToolBox.StringToInt(characterInfo.Name));
ISpatialEntity gotoTarget = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Human, humanPrefab.GetModuleFlags(), humanPrefab.GetSpawnPointTags());
if (gotoTarget == null)
{
gotoTarget = outpost.GetHulls(true).GetRandom();
}
characterInfo.TeamID = Character.TeamType.FriendlyNPC;
characterInfo.TeamID = CharacterTeamType.FriendlyNPC;
var npc = Character.Create(CharacterPrefab.HumanConfigFile, SpawnAction.OffsetSpawnPos(gotoTarget.WorldPosition, 100.0f), ToolBox.RandomSeed(8), characterInfo, hasAi: true, createNetworkEvent: true);
npc.AnimController.FindHull(gotoTarget.WorldPosition, true);
npc.TeamID = Character.TeamType.FriendlyNPC;
npc.TeamID = CharacterTeamType.FriendlyNPC;
if (!outpost.Info.OutpostNPCs.ContainsKey(humanPrefab.Identifier))
{
outpost.Info.OutpostNPCs.Add(humanPrefab.Identifier, new List<Character>());
@@ -1404,29 +1414,12 @@ namespace Barotrauma
npc.CharacterHealth.MaxVitality *= humanPrefab.HealthMultiplier;
}
humanPrefab.GiveItems(npc, outpost, Rand.RandSync.Server);
foreach (Item item in npc.Inventory.Items)
foreach (Item item in npc.Inventory.FindAllItems(it => it != null, recursive: true))
{
if (item != null) { item.SpawnedInOutpost = true; }
item.SpawnedInOutpost = !outpost.Info.OutpostGenerationParams.AllowStealing;
}
npc.GiveIdCardTags(gotoTarget as WayPoint);
if (npc.AIController is HumanAIController humanAI)
{
var idleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
if (humanPrefab.CampaignInteractionType != CampaignMode.InteractionType.None)
{
idleObjective.Behavior = AIObjectiveIdle.BehaviorType.StayInHull;
idleObjective.TargetHull = AIObjectiveGoTo.GetTargetHull(gotoTarget);
(GameMain.GameSession.GameMode as CampaignMode)?.AssignNPCMenuInteraction(npc, humanPrefab.CampaignInteractionType);
}
else
{
idleObjective.Behavior = humanPrefab.Behavior;
foreach (string moduleType in humanPrefab.PreferredOutpostModuleTypes)
{
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
}
humanPrefab.InitializeCharacter(npc, gotoTarget);
}
}
}
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
@@ -13,6 +12,14 @@ namespace Barotrauma
public readonly int MinAvailableAmount;
//maximum number of items available at a given store
public readonly int MaxAvailableAmount;
/// <summary>
/// Used when both <see cref="MinAvailableAmount"/> and <see cref="MaxAvailableAmount"/> are set to 0.
/// </summary>
public const int DefaultAmount = 5;
/// <summary>
/// Can the item be a Daily Special or a Requested Good
/// </summary>
public readonly bool CanBeSpecial;
/// <summary>
/// Support for the old style of determining item prices
@@ -23,16 +30,21 @@ namespace Barotrauma
{
Price = element.GetAttributeInt("buyprice", 0);
CanBeBought = true;
MinAvailableAmount = GetMinAmount(element);
MaxAvailableAmount = GetMaxAmount(element);
var minAmount = GetMinAmount(element);
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
var maxAmount = GetMaxAmount(element);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount);
}
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0)
public PriceInfo(int price, bool canBeBought, int minAmount = 0, int maxAmount = 0, bool canBeSpecial = true)
{
Price = price;
CanBeBought = canBeBought;
MinAvailableAmount = minAmount;
MaxAvailableAmount = maxAmount;
MinAvailableAmount = Math.Min(minAmount, CargoManager.MaxQuantity);
maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity);
MaxAvailableAmount = Math.Max(maxAmount, minAmount);
CanBeSpecial = canBeSpecial;
}
public static List<Tuple<string, PriceInfo>> CreatePriceInfos(XElement element, out PriceInfo defaultPrice)
@@ -42,6 +54,7 @@ namespace Barotrauma
var soldByDefault = element.GetAttributeBool("soldbydefault", true);
var minAmount = GetMinAmount(element);
var maxAmount = GetMaxAmount(element);
var canBeSpecial = element.GetAttributeBool("canbespecial", true);
var priceInfos = new List<Tuple<string, PriceInfo>>();
foreach (XElement childElement in element.GetChildElements("price"))
@@ -51,13 +64,15 @@ namespace Barotrauma
priceInfos.Add(new Tuple<string, PriceInfo>(childElement.GetAttributeString("locationtype", "").ToLowerInvariant(),
new PriceInfo(price: (int)(priceMultiplier * basePrice), canBeBought: sold,
minAmount: sold ? GetMinAmount(childElement, minAmount) : 0,
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0)));
maxAmount: sold ? GetMaxAmount(childElement, maxAmount) : 0,
canBeSpecial: canBeSpecial)));
}
var canBeBoughtAtOtherLocations = soldByDefault && element.GetAttributeBool("soldeverywhere", true);
defaultPrice = new PriceInfo(basePrice, canBeBoughtAtOtherLocations,
minAmount: canBeBoughtAtOtherLocations ? minAmount : 0,
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0);
maxAmount: canBeBoughtAtOtherLocations ? maxAmount : 0,
canBeSpecial: canBeSpecial);
return priceInfos;
}
@@ -14,12 +14,11 @@ using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
partial class WallSection : ISpatialEntity
partial class WallSection : IIgnorable
{
public Rectangle rect;
public float damage;
public Gap gap;
private bool ignoreByAI;
public Structure Wall { get; }
public Vector2 Position => Wall.SectionPosition(Wall.Sections.IndexOf(this));
@@ -28,7 +27,8 @@ namespace Barotrauma
public Submarine Submarine => Wall.Submarine;
public Rectangle WorldRect => Submarine == null ? rect :
new Rectangle((int)(rect.X + Submarine.Position.X), (int)(rect.Y + Submarine.Position.Y), rect.Width, rect.Height);
public bool IgnoreByAI => ignoreByAI;
public bool IgnoreByAI => OrderedToBeIgnored;
public bool OrderedToBeIgnored { get; set; }
public WallSection(Rectangle rect, Structure wall, float damage = 0.0f)
{
@@ -37,8 +37,6 @@ namespace Barotrauma
this.damage = damage;
Wall = wall;
}
public void SetIgnoreByAI(bool ignore) => ignoreByAI = ignore;
}
partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity
@@ -144,10 +142,16 @@ namespace Barotrauma
{
get
{
return Prefab.Body && !IsPlatform;
return Prefab.Body && !IsPlatform;// && HasDamage;
}
}
public bool HasDamage
{
get;
private set;
}
public StructurePrefab Prefab => prefab as StructurePrefab;
public HashSet<string> Tags
@@ -356,7 +360,7 @@ namespace Barotrauma
#endif
}
public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine, ushort id = Entity.NullEntityID)
public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine, ushort id = Entity.NullEntityID, XElement element = null)
: base(sp, submarine, id)
{
System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0);
@@ -395,7 +399,6 @@ namespace Barotrauma
StairDirection = Prefab.StairDirection;
NoAITarget = Prefab.NoAITarget;
SerializableProperties = SerializableProperty.GetProperties(this);
InitProjSpecific();
@@ -421,6 +424,8 @@ namespace Barotrauma
}
}
SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this);
// Only add ai targets automatically to submarine/outpost walls
if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget)
{
@@ -632,7 +637,7 @@ namespace Barotrauma
Vector2 bodyPos = WorldPosition + BodyOffset;
Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, MathHelper.ToDegrees(BodyRotation));
Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation);
return
Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
@@ -836,7 +841,7 @@ namespace Barotrauma
public int FindSectionIndex(Vector2 displayPos, bool world = false, bool clamp = false)
{
if (!Sections.Any()) return -1;
if (Sections.None()) { return -1; }
if (world && Submarine != null)
{
@@ -850,7 +855,7 @@ namespace Barotrauma
displayPos.X += WallSectionSize - Sections[0].rect.Width;
}
int index = (IsHorizontal) ?
int index = IsHorizontal ?
(int)Math.Floor((displayPos.X - rect.X) / WallSectionSize) :
(int)Math.Floor((rect.Y - displayPos.Y) / WallSectionSize);
@@ -944,14 +949,14 @@ namespace Barotrauma
return new AttackResult(damageAmount, null);
}
private void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
public void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
{
if (Submarine != null && Submarine.GodMode || Indestructible) { return; }
if (!Prefab.Body) { return; }
if (!MathUtils.IsValid(damage)) { return; }
damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
#if SERVER
if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage)
{
@@ -1065,15 +1070,17 @@ namespace Barotrauma
}
float gapOpen = (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
Sections[sectionIndex].gap.Open = gapOpen;
Sections[sectionIndex].gap.Open = gapOpen;
}
float damageDiff = damage - Sections[sectionIndex].damage;
bool hadHole = SectionBodyDisabled(sectionIndex);
Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, MaxHealth);
HasDamage = Sections.Any(s => s.damage > 0.0f);
if (attacker != null && damageDiff != 0.0f)
{
HumanAIController.StructureDamaged(this, damageDiff, attacker);
OnHealthChangedProjSpecific(attacker, damageDiff);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -1081,14 +1088,14 @@ namespace Barotrauma
{
attacker.Info.IncreaseSkillLevel("mechanical",
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f),
SectionPosition(sectionIndex, true));
SectionPosition(sectionIndex));
}
}
}
bool hasHole = SectionBodyDisabled(sectionIndex);
if (hadHole == hasHole) return;
if (hadHole == hasHole) { return; }
UpdateSections();
}
@@ -1283,18 +1290,17 @@ namespace Barotrauma
}
Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
Structure s = new Structure(rect, prefab, submarine, idRemap.GetOffsetId(element))
Structure s = new Structure(rect, prefab, submarine, idRemap.GetOffsetId(element), element)
{
Submarine = submarine,
};
SerializableProperty.DeserializeProperties(s, element);
if (submarine?.Info.GameVersion != null)
{
SerializableProperty.UpgradeGameVersion(s, s.Prefab.ConfigElement, submarine.Info.GameVersion);
}
bool hasDamage = false;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -1311,7 +1317,9 @@ namespace Barotrauma
}
else
{
s.Sections[index].damage = subElement.GetAttributeFloat("damage", 0.0f);
float damage = subElement.GetAttributeFloat("damage", 0.0f);
s.Sections[index].damage = damage;
hasDamage |= damage > 0.0f;
}
break;
case "upgrade":
@@ -1333,8 +1341,8 @@ namespace Barotrauma
}
}
if (element.GetAttributeBool("flippedx", false)) s.FlipX(false);
if (element.GetAttributeBool("flippedy", false)) s.FlipY(false);
if (element.GetAttributeBool("flippedx", false)) { s.FlipX(false); }
if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); }
//structures with a body drop a shadow by default
if (element.Attribute("usedropshadow") == null)
@@ -1347,6 +1355,11 @@ namespace Barotrauma
s.NoAITarget = prefab.NoAITarget;
}
if (hasDamage)
{
s.UpdateSections();
}
return s;
}
@@ -360,7 +360,8 @@ namespace Barotrauma
}
}
if (!Enum.TryParse(element.GetAttributeString("category", "Structure"), true, out MapEntityCategory category))
string categoryStr = element.GetAttributeString("category", "Structure");
if (!Enum.TryParse(categoryStr, true, out MapEntityCategory category))
{
category = MapEntityCategory.Structure;
}
@@ -419,6 +420,13 @@ namespace Barotrauma
}
}
//backwards compatibility
if (categoryStr.Equals("Thalamus", StringComparison.OrdinalIgnoreCase))
{
sp.Category = MapEntityCategory.Wrecked;
sp.Subcategory = "Thalamus";
}
if (string.IsNullOrEmpty(sp.identifier))
{
DebugConsole.ThrowError(
@@ -25,7 +25,7 @@ namespace Barotrauma
{
public SubmarineInfo Info { get; private set; }
public Character.TeamType TeamID = Character.TeamType.None;
public CharacterTeamType TeamID = CharacterTeamType.None;
public static readonly Vector2 HiddenSubStartPosition = new Vector2(-50000.0f, 10000.0f);
//position of the "actual submarine" which is rendered wherever the SubmarineBody is
@@ -254,7 +254,7 @@ namespace Barotrauma
get
{
if (Level.Loaded == null || subBody == null) { return false; }
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth;
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth & RealWorldDepth > RealWorldCrushDepth;
}
}
@@ -304,7 +304,6 @@ namespace Barotrauma
{
if ((!anyHasTag || item.HasTag("ballast")) && item.GetComponent<Pump>() is { } pump)
{
if (pump.Infected) { continue; }
pumps.Add(pump);
}
}
@@ -312,11 +311,13 @@ namespace Barotrauma
if (!pumps.Any()) { return; }
Pump randomPump = pumps.GetRandom(Rand.RandSync.Unsynced);
randomPump.Infected = true;
randomPump.InfectIdentifier = identifier;
if (randomPump.IsOn && randomPump.HasPower && randomPump.FlowPercentage > 0 && randomPump.Item.Condition > 0.0f)
{
randomPump.InfectBallast(identifier);
#if SERVER
randomPump.Item.CreateServerEvent(randomPump);
randomPump.Item.CreateServerEvent(randomPump);
#endif
}
}
public void MakeWreck()
@@ -324,7 +325,7 @@ namespace Barotrauma
Info.Type = SubmarineType.Wreck;
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = Character.TeamType.None;
TeamID = CharacterTeamType.None;
string defaultTag = Level.Loaded.GetWreckIDTag("wreck_id", this);
ReplaceIDCardTagRequirements("wreck_id", defaultTag);
@@ -553,7 +554,7 @@ namespace Barotrauma
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
}
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - 10);
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2);
return spawnPos - diffFromDockedBorders;
}
@@ -586,6 +587,7 @@ namespace Barotrauma
{
if (e is Item item)
{
if (item.GetComponent<Turret>() != null) { return false; }
if (item.body != null && !item.body.Enabled) { return true; }
}
return false;
@@ -598,6 +600,17 @@ namespace Barotrauma
for (int i = 1; i < entities.Count; i++)
{
if (entities[i] is Item item)
{
var turret = item.GetComponent<Turret>();
if (turret != null)
{
minX = Math.Min(minX, entities[i].Rect.X + turret.TransformedBarrelPos.X * 2f);
minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height - turret.TransformedBarrelPos.Y * 2f);
maxX = Math.Max(maxX, entities[i].Rect.Right + turret.TransformedBarrelPos.X * 2f);
maxY = Math.Max(maxY, entities[i].Rect.Y - turret.TransformedBarrelPos.Y * 2f);
}
}
minX = Math.Min(minX, entities[i].Rect.X);
minY = Math.Min(minY, entities[i].Rect.Y - entities[i].Rect.Height);
maxX = Math.Max(maxX, entities[i].Rect.Right);
@@ -1136,7 +1149,7 @@ namespace Barotrauma
{
if (ConnectedDockingPorts.TryGetValue(dockedSub, out DockingPort port))
{
port.Undock();
port.Undock(applyEffects: false);
continue;
}
}
@@ -1277,11 +1290,7 @@ namespace Barotrauma
HiddenSubPosition += Vector2.UnitY * (sub.Borders.Height + 5000.0f);
}
IdOffset = 0;
foreach (MapEntity me in MapEntity.mapEntityList)
{
IdOffset = Math.Max(IdOffset, me.ID);
}
IdOffset = IdRemap.DetermineNewOffset();
List<MapEntity> newEntities = new List<MapEntity>();
if (loadEntities == null)
@@ -1335,16 +1344,20 @@ namespace Barotrauma
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = Character.TeamType.FriendlyNPC;
TeamID = CharacterTeamType.FriendlyNPC;
bool indestructible =
GameMain.NetworkMember != null &&
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
foreach (MapEntity me in MapEntity.mapEntityList)
{
if (me.Submarine != this) { continue; }
if (me is Item item)
{
item.SpawnedInOutpost = true;
if (item.GetComponent<Repairable>() != null &&
(GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts))
item.SpawnedInOutpost = !info.OutpostGenerationParams.AllowStealing;
if (item.GetComponent<Repairable>() != null && indestructible)
{
item.Indestructible = true;
}
@@ -1353,7 +1366,10 @@ namespace Barotrauma
if (ic is ConnectionPanel connectionPanel)
{
//prevent rewiring
connectionPanel.Locked = true;
if (!info.OutpostGenerationParams.AlwaysRewireable)
{
connectionPanel.Locked = true;
}
}
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
{
@@ -1366,9 +1382,9 @@ namespace Barotrauma
}
}
}
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts)
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
{
structure.Indestructible = GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.DestructibleOutposts;
structure.Indestructible = true;
}
}
}
@@ -1498,7 +1514,19 @@ namespace Barotrauma
if (e is Item item)
{
if (item.FindParentInventory(inv => inv is CharacterInventory) != null) { continue; }
#if CLIENT
if (Screen.Selected != GameMain.SubEditorScreen)
{
if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; }
}
else
{
e.Submarine = this;
}
#else
if (e.Submarine != this && item.GetRootContainer()?.Submarine != this) { continue; }
#endif
}
else
{
@@ -1520,6 +1548,10 @@ namespace Barotrauma
OutpostModuleInfo = Info.OutpostModuleInfo != null ? new OutpostModuleInfo(Info.OutpostModuleInfo) : null,
Name = Path.GetFileNameWithoutExtension(filePath)
};
#if CLIENT
//remove reference to the preview image from the old info, so we don't dispose it (the new info still uses the texture)
Info.PreviewImage = null;
#endif
Info.Dispose(); Info = newInfo;
return newInfo.SaveAs(filePath, previewImage);
}
@@ -1694,6 +1726,7 @@ namespace Barotrauma
}
}
}
node.Waypoint.FindHull();
}
}
@@ -1708,6 +1741,7 @@ namespace Barotrauma
nodes.Clear();
obstructedNodes.Remove(otherSub);
}
OutdoorNodes.ForEach(n => n.Waypoint.FindHull());
}
}
}
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
@@ -446,21 +447,24 @@ namespace Barotrauma
private void UpdateDepthDamage(float deltaTime)
{
#if CLIENT
if (GameMain.GameSession.GameMode is TestGameMode) { return; }
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
#endif
if (Level.Loaded == null) { return; }
float submarineDepth = submarine.RealWorldDepth;
if (submarineDepth < Level.Loaded.RealWorldCrushDepth) { return; }
if (!Submarine.AtDamageDepth) { return; }
depthDamageTimer -= deltaTime;
if (depthDamageTimer > 0.0f) { return; }
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine != submarine || wall.CrushDepth > submarineDepth) { continue; }
if (wall.Submarine != submarine) { continue; }
float pastCrushDepth = submarineDepth - wall.CrushDepth;
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f);
float wallCrushDepth = wall.CrushDepth;
if (submarine.Info.SubmarineClass == SubmarineClass.DeepDiver) { wallCrushDepth *= 1.2f; }
float pastCrushDepth = submarine.RealWorldDepth - wallCrushDepth;
if (pastCrushDepth < 0) { return; }
Explosion.RangedStructureDamage(wall.WorldPosition, 100.0f, pastCrushDepth * 0.1f, levelWallDamage: 0.0f);
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, Math.Min(pastCrushDepth * 0.001f, 50.0f));
@@ -555,19 +559,29 @@ namespace Barotrauma
{
if (limb?.body?.FarseerBody == null || limb.character == null) { return; }
if (limb.Mass > MinImpactLimbMass)
float impactMass = limb.Mass;
var enemyAI = limb.character.AIController as EnemyAIController;
float attackMultiplier = 1.0f;
if (enemyAI?.ActiveAttack != null)
{
impactMass = Math.Max(Math.Max(limb.Mass, limb.character.AnimController.MainLimb.Mass), limb.character.AnimController.Collider.Mass);
attackMultiplier = enemyAI.ActiveAttack.SubmarineImpactMultiplier;
}
if (impactMass * attackMultiplier > MinImpactLimbMass)
{
Vector2 normal =
Vector2.DistanceSquared(Body.SimPosition, limb.SimPosition) < 0.0001f ?
Vector2.UnitY :
Vector2.Normalize(Body.SimPosition - limb.SimPosition);
float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(limb.Mass / 100.0f, 1);
float impact = Math.Min(Vector2.Dot(collision.Velocity, -normal), 50.0f) * Math.Min(impactMass / 300.0f, 1);
impact *= attackMultiplier;
ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
foreach (Submarine dockedSub in submarine.DockedTo)
{
dockedSub.SubBody.ApplyImpact(impact, -normal, collision.ImpactPos, applyDamage: false);
dockedSub.SubBody.ApplyImpact(impact, normal, collision.ImpactPos, applyDamage: false);
}
}
@@ -663,7 +677,7 @@ namespace Barotrauma
dockedSub.SubBody.ApplyImpact(wallImpact, -impact.Normal, impact.ImpactPos);
}
if (cell != null && wallImpact > 0.0f)
if (cell != null && cell.IsDestructible && wallImpact > 0.0f)
{
var hitWall = Level.Loaded?.ExtraWalls.Find(w => w.Cells.Contains(cell));
if (hitWall != null && hitWall.WallDamageOnTouch > 0.0f)
@@ -672,7 +686,7 @@ namespace Barotrauma
ConvertUnits.ToDisplayUnits(impact.ImpactPos),
500.0f,
hitWall.WallDamageOnTouch,
damageLevelWalls: false);
levelWallDamage: 0.0f);
#if CLIENT
PlayDamageSounds(damagedStructures, impact.ImpactPos, wallImpact, "StructureSlash");
#endif
@@ -800,7 +814,7 @@ namespace Barotrauma
#if CLIENT
if (Character.Controlled != null && Character.Controlled.Submarine == submarine)
{
GameMain.GameScreen.Cam.Shake = impact * 2.0f;
GameMain.GameScreen.Cam.Shake = impact * 10.0f;
if (submarine.Info.Type == SubmarineType.Player && !submarine.DockedTo.Any(s => s.Info.Type != SubmarineType.Player))
{
float angularVelocity =
@@ -814,25 +828,32 @@ namespace Barotrauma
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != submarine) { continue; }
if (c.KnockbackCooldownTimer > 0.0f) { continue; }
c.KnockbackCooldownTimer = Character.KnockbackCooldown;
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
limb.body.ApplyLinearImpulse(limb.Mass * impulse, 10.0f);
}
c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
bool holdingOntoSomething = false;
if (c.SelectedConstruction != null)
{
var controller = c.SelectedConstruction.GetComponent<Items.Components.Controller>();
holdingOntoSomething = controller != null && controller.LimbPositions.Any();
holdingOntoSomething =
c.SelectedConstruction.GetComponent<Ladder>() != null ||
(c.SelectedConstruction.GetComponent<Controller>()?.LimbPositions.Any() ?? false);
}
//stun for up to 1 second if the impact equal or higher to the maximum impact
if (impact >= MaxCollisionImpact && !holdingOntoSomething)
if (!holdingOntoSomething)
{
c.SetStun(Math.Min(impulse.Length() * 0.2f, 1.0f));
c.AnimController.Collider.ApplyLinearImpulse(c.AnimController.Collider.Mass * impulse, 10.0f);
//stun for up to 2 second if the impact equal or higher to the maximum impact
if (impact >= MaxCollisionImpact)
{
c.AddDamage(impactPos, AfflictionPrefab.ImpactDamage.Instantiate(3.0f).ToEnumerable(), stun: Math.Min(impulse.Length() * 0.2f, 2.0f), playSound: true);
}
}
}
@@ -843,11 +864,12 @@ namespace Barotrauma
item.body.ApplyLinearImpulse(item.body.Mass * impulse, 10.0f);
}
float dmg = applyDamage ? impact * ImpactDamageMultiplier : 0.0f;
var damagedStructures = Explosion.RangedStructureDamage(
ConvertUnits.ToDisplayUnits(impactPos),
impact * 50.0f,
applyDamage ? impact * ImpactDamageMultiplier : 0.0f);
impact * 50.0f,
dmg, dmg);
#if CLIENT
PlayDamageSounds(damagedStructures, impactPos, impact, "StructureBlunt");
@@ -108,8 +108,11 @@ namespace Barotrauma
{
if (hash == null)
{
XDocument doc = OpenFile(FilePath);
StartHashDocTask(doc);
if (hashTask == null)
{
XDocument doc = OpenFile(FilePath);
StartHashDocTask(doc);
}
hashTask.Wait();
hashTask = null;
}
@@ -118,6 +121,11 @@ namespace Barotrauma
}
}
public bool CalculatingHash
{
get { return hashTask != null && !hashTask.IsCompleted; }
}
public Vector2 Dimensions
{
get;
@@ -373,6 +381,10 @@ namespace Barotrauma
public void Dispose()
{
#if CLIENT
PreviewImage?.Remove();
PreviewImage = null;
#endif
if (savedSubmarines.Contains(this)) { savedSubmarines.Remove(this); }
}
@@ -522,12 +534,13 @@ namespace Barotrauma
for (int i = savedSubmarines.Count - 1; i >= 0; i--)
{
if (File.Exists(savedSubmarines[i].FilePath) &&
savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) &&
(Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath) ||
contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath())))
if (File.Exists(savedSubmarines[i].FilePath))
{
continue;
bool isDownloadedSub = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
bool isInSubmarinesFolder = Path.GetFullPath(Path.GetDirectoryName(savedSubmarines[i].FilePath)) == Path.GetFullPath(SavePath);
bool isInContentPackage = contentPackageSubs.Any(fp => Path.GetFullPath(fp.Path).CleanUpPath() == Path.GetFullPath(savedSubmarines[i].FilePath).CleanUpPath());
if (isDownloadedSub) { continue; }
if (savedSubmarines[i].LastModifiedTime == File.GetLastWriteTime(savedSubmarines[i].FilePath) && (isInSubmarinesFolder || isInContentPackage)) { continue; }
}
savedSubmarines[i].Dispose();
}
@@ -18,7 +18,7 @@ namespace Barotrauma
public static bool ShowWayPoints = true, ShowSpawnPoints = true;
public const float LadderWaypointInterval = 100.0f;
public const float LadderWaypointInterval = 70.0f;
protected SpawnType spawnType;
private string[] idCardTags;
@@ -44,6 +44,8 @@ namespace Barotrauma
public Hull CurrentHull { get; private set; }
public Level.Tunnel Tunnel;
public SpawnType SpawnType
{
get { return spawnType; }
@@ -97,6 +99,13 @@ namespace Barotrauma
{
SpawnType = SpawnType.Path;
}
#if CLIENT
if (SubEditorScreen.IsSubEditor())
{
SubEditorScreen.StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { this }, false));
}
#endif
}
@@ -177,42 +186,55 @@ namespace Barotrauma
}
}
float diffFromHullEdge = 50;
float minDist = 150.0f;
float minDist = 100.0f;
float heightFromFloor = 110.0f;
float hullMinHeight = 100;
foreach (Hull hull in Hull.hullList)
{
if (hull.Rect.Height < 150) { continue; }
WayPoint prevWaypoint = null;
// Ignore hulls that a human couldn't fit in.
// Doesn't take multi-hull rooms into account, but it's probably best to leave them to be setup manually.
if (hull.Rect.Height < hullMinHeight) { continue; }
// Don't create waypoints if there's no floor.
Vector2 floorPos = new Vector2(hull.SimPosition.X, ConvertUnits.ToSimUnits(hull.Rect.Y - hull.RectHeight - 50));
Body floor = Submarine.PickBody(hull.SimPosition, floorPos, collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform, customPredicate: f => !(f.Body.UserData is Submarine));
if (floor == null) { continue; }
// Make sure that the waypoints don't go higher than the halfway of the room.
float waypointHeight = hull.Rect.Height > heightFromFloor * 2 ? heightFromFloor : hull.Rect.Height / 2;
if (hull.Rect.Width < diffFromHullEdge * 3.0f)
{
new WayPoint(
new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
continue;
new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
}
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
else
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + heightFromFloor), SpawnType.Path, submarine);
if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); }
prevWaypoint = wayPoint;
WayPoint prevWaypoint = null;
for (float x = hull.Rect.X + diffFromHullEdge; x <= hull.Rect.Right - diffFromHullEdge; x += minDist)
{
var wayPoint = new WayPoint(new Vector2(x, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
if (prevWaypoint != null) { wayPoint.ConnectTo(prevWaypoint); }
prevWaypoint = wayPoint;
}
if (prevWaypoint == null)
{
// Ensure that we always create at least one waypoint per hull.
new WayPoint(new Vector2(hull.Rect.X + hull.Rect.Width / 2.0f, hull.Rect.Y - hull.Rect.Height + waypointHeight), SpawnType.Path, submarine);
}
}
}
float outSideWaypointInterval = 200.0f;
float outSideWaypointInterval = 100.0f;
if (submarine.Info.Type != SubmarineType.OutpostModule)
{
int outsideWaypointDist = 100;
List<WayPoint> outsideWaypoints = new List<WayPoint>();
Rectangle borders = Hull.GetBorders();
borders.X -= outsideWaypointDist;
borders.Y += outsideWaypointDist;
borders.Width += outsideWaypointDist * 2;
borders.Height += outsideWaypointDist * 2;
int originalWidth = borders.Width;
int originalHeight = borders.Height;
borders.X -= Math.Min(500, originalWidth / 4);
borders.Y += Math.Min(500, originalHeight / 4);
borders.Width += Math.Min(1500, originalWidth / 2);
borders.Height += Math.Min(1000, originalHeight / 2);
borders.Location -= MathUtils.ToPoint(submarine.HiddenSubPosition);
if (borders.Width <= outSideWaypointInterval * 2)
@@ -236,6 +258,8 @@ namespace Barotrauma
new Vector2(x, borders.Y - borders.Height * i) + submarine.HiddenSubPosition,
SpawnType.Path, submarine);
outsideWaypoints.Add(wayPoint);
if (x == borders.X + outSideWaypointInterval)
{
cornerWaypoint[i, 0] = wayPoint;
@@ -258,18 +282,107 @@ namespace Barotrauma
new Vector2(borders.X + borders.Width * i, y) + submarine.HiddenSubPosition,
SpawnType.Path, submarine);
outsideWaypoints.Add(wayPoint);
if (y == borders.Y - borders.Height)
{
wayPoint.ConnectTo(cornerWaypoint[1, i]);
}
else
{
wayPoint.ConnectTo(WayPoint.WayPointList[WayPointList.Count - 2]);
wayPoint.ConnectTo(WayPointList[WayPointList.Count - 2]);
}
}
wayPoint.ConnectTo(cornerWaypoint[0, i]);
}
Vector2 center = ConvertUnits.ToSimUnits(submarine.HiddenSubPosition);
float halfHeight = ConvertUnits.ToSimUnits(borders.Height / 2);
// Try to move the waypoints so that they are near the walls, roughly following the shape of the sub.
foreach (WayPoint wp in outsideWaypoints)
{
float xDiff = center.X - wp.SimPosition.X;
Vector2 targetPos = new Vector2(center.X - xDiff * 0.5f, center.Y);
Body wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
if (wall == null)
{
// Try again, and shoot to the center now. It happens with some subs that the first, offset raycast don't hit the walls.
targetPos = new Vector2(center.X - xDiff, center.Y);
wall = Submarine.PickBody(wp.SimPosition, targetPos, collisionCategory: Physics.CollisionWall, customPredicate: f => !(f.Body.UserData is Submarine));
}
if (wall != null)
{
float distanceFromWall = 1;
if (xDiff > 0 && !submarine.Info.HasTag(SubmarineTag.Shuttle))
{
// We don't want to move the waypoints near the tail too close to the engine.
float yDist = Math.Abs(center.Y - wp.SimPosition.Y);
distanceFromWall = MathHelper.Lerp(1, 3, MathUtils.InverseLerp(halfHeight, 0, yDist));
}
Vector2 newPos = Submarine.LastPickedPosition + Submarine.LastPickedNormal * distanceFromWall;
wp.rect = new Rectangle(ConvertUnits.ToDisplayUnits(newPos).ToPoint(), wp.rect.Size);
wp.FindHull();
}
}
// Remove unwanted points
var removals = new List<WayPoint>();
WayPoint previous = null;
float tooClose = outSideWaypointInterval / 2;
foreach (WayPoint wp in outsideWaypoints)
{
if (wp.CurrentHull != null ||
Submarine.PickBody(wp.SimPosition, wp.SimPosition + Vector2.Normalize(center - wp.SimPosition) * 0.1f, collisionCategory: Physics.CollisionWall | Physics.CollisionItem, customPredicate: f => !(f.Body.UserData is Submarine), allowInsideFixture: true) != null)
{
// Remove waypoints that got inside/too near the sub.
removals.Add(wp);
previous = wp;
continue;
}
foreach (WayPoint otherWp in outsideWaypoints)
{
if (otherWp == wp) { continue; }
if (removals.Contains(otherWp)) { continue; }
float sqrDist = Vector2.DistanceSquared(wp.Position, otherWp.Position);
// Remove waypoints that are too close to each other.
if (!removals.Contains(previous) && sqrDist < tooClose * tooClose)
{
removals.Add(wp);
}
}
previous = wp;
}
foreach (WayPoint wp in removals)
{
outsideWaypoints.Remove(wp);
wp.Remove();
}
// Connect loose ends (TODO: this sometimes fails, creating the connection to a wrong node)
for (int i = 0; i < outsideWaypoints.Count; i++)
{
WayPoint current = outsideWaypoints[i];
if (current.linkedTo.Count > 1) { continue; }
WayPoint next = null;
int maxConnections = 2;
float tooFar = outSideWaypointInterval * 5;
for (int j = 0; j < maxConnections; j++)
{
if (current.linkedTo.Count >= maxConnections) { break; }
tooFar /= current.linkedTo.Count;
// First try to find a loose end
next = current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e)) && wp.linkedTo.Count < 2);
// Then accept any connection that not connected to the existing connection
next ??= current.FindClosestOutside(outsideWaypoints, tolerance: tooFar, filter: wp => wp != next && wp.linkedTo.None(e => current.linkedTo.Contains(e)));
if (next != null)
{
current.ConnectTo(next);
}
}
if (current.linkedTo.Count == 1)
{
DebugConsole.ThrowError($"Couldn't automatically link waypoint {current.ID}. You should do it manually.");
}
}
}
List<Structure> stairList = new List<Structure>();
@@ -296,13 +409,13 @@ namespace Barotrauma
{
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = stairPoints[i].FindClosest(dir, true, new Vector2(-30.0f, 30f));
if (closest == null) continue;
WayPoint closest = stairPoints[i].FindClosest(dir, horizontalSearch: true, new Vector2(100, 70));
if (closest == null) { continue; }
stairPoints[i].ConnectTo(closest);
}
}
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position)/2, SpawnType.Path, submarine);
stairPoints[2] = new WayPoint((stairPoints[0].Position + stairPoints[1].Position) / 2, SpawnType.Path, submarine);
stairPoints[0].ConnectTo(stairPoints[2]);
stairPoints[2].ConnectTo(stairPoints[1]);
}
@@ -312,21 +425,44 @@ namespace Barotrauma
var ladders = item.GetComponent<Ladder>();
if (ladders == null) { continue; }
Vector2 bottomPoint = new Vector2(item.Rect.Center.X, item.Rect.Top - item.Rect.Height + 10);
List<WayPoint> ladderPoints = new List<WayPoint>
{
new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - item.Rect.Height + heightFromFloor), SpawnType.Path, submarine)
new WayPoint(bottomPoint, SpawnType.Path, submarine),
};
WayPoint prevPoint = ladderPoints[0];
Vector2 prevPos = prevPoint.SimPosition;
List<Body> ignoredBodies = new List<Body>();
for (float y = ladderPoints[0].Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval)
// Lowest point is only meaningful for hanging ladders inside the sub, but it shouldn't matter in other cases either.
// Start point is where the bots normally grasp the ladder when they stand on ground.
WayPoint lowestPoint = ladderPoints[0];
WayPoint prevPoint = lowestPoint;
Vector2 prevPos = prevPoint.SimPosition;
Body ground = Submarine.PickBody(lowestPoint.SimPosition, lowestPoint.SimPosition - Vector2.UnitY, ignoredBodies,
collisionCategory: Physics.CollisionWall | Physics.CollisionPlatform | Physics.CollisionStairs,
customPredicate: f => !(f.Body.UserData is Submarine));
float startHeight = ground != null ? ConvertUnits.ToDisplayUnits(ground.Position.Y) : bottomPoint.Y;
startHeight += heightFromFloor;
WayPoint startPoint = lowestPoint;
Vector2 nextPos = new Vector2(item.Rect.Center.X, startHeight);
// Don't create the start point if it's too close to the lowest point or if it's outside of the sub.
// If we skip creating the start point, the lowest point is used instead.
if (lowestPoint == null || Math.Abs(startPoint.Position.Y - startHeight) > 40 && Hull.FindHull(nextPos) != null)
{
startPoint = new WayPoint(nextPos, SpawnType.Path, submarine);
ladderPoints.Add(startPoint);
if (lowestPoint != null)
{
startPoint.ConnectTo(lowestPoint);
}
prevPoint = startPoint;
prevPos = prevPoint.SimPosition;
}
for (float y = startPoint.Position.Y + LadderWaypointInterval; y < item.Rect.Y - 1.0f; y += LadderWaypointInterval)
{
//first check if there's a door in the way
//(we need to create a waypoint linked to the door for NPCs to open it)
Body pickedBody = Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)),
ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)),
prevPos, ignoredBodies, Physics.CollisionWall, false,
(Fixture f) => f.Body.UserData is Item && ((Item)f.Body.UserData).GetComponent<Door>() != null);
@@ -339,7 +475,7 @@ namespace Barotrauma
{
//no door, check for walls
pickedBody = Submarine.PickBody(
ConvertUnits.ToSimUnits(new Vector2(ladderPoints[0].Position.X, y)), prevPos, ignoredBodies, null, false,
ConvertUnits.ToSimUnits(new Vector2(startPoint.Position.X, y)), prevPos, ignoredBodies, null, false,
(Fixture f) => f.Body.UserData is Structure);
}
@@ -372,75 +508,94 @@ namespace Barotrauma
}
}
if (prevPoint.rect.Y < item.Rect.Y - 10.0f)
// Cap
if (prevPoint.rect.Y < item.Rect.Y - 40)
{
WayPoint newPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
ladderPoints.Add(newPoint);
newPoint.ConnectTo(prevPoint);
WayPoint wayPoint = new WayPoint(new Vector2(item.Rect.Center.X, item.Rect.Y - 1.0f), SpawnType.Path, submarine);
ladderPoints.Add(wayPoint);
wayPoint.ConnectTo(prevPoint);
}
//connect ladder waypoints to hull points at the right and left side
// Connect ladder waypoints to hull points at the right and left side
foreach (WayPoint ladderPoint in ladderPoints)
{
ladderPoint.Ladders = ladders;
//don't connect if the waypoint is at a gap (= at the boundary of hulls and/or at a hatch)
if (ladderPoint.ConnectedGap != null) continue;
bool isHatch = ladderPoint.ConnectedGap != null && !ladderPoint.ConnectedGap.IsRoomToRoom;
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = ladderPoint.FindClosest(dir, true, new Vector2(-150.0f, 50f));
if (closest == null) continue;
WayPoint closest = null;
if (isHatch)
{
closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null, ignored: ladderPoints);
}
else
{
closest = ladderPoint.FindClosest(dir, horizontalSearch: true, new Vector2(150, 70), ladderPoint.ConnectedGap?.ConnectedDoor?.Body.FarseerBody, ignored: ladderPoints);
}
if (closest == null) { continue; }
ladderPoint.ConnectTo(closest);
}
}
}
foreach (Gap gap in Gap.GapList)
// Another pass: connect cap and bottom points with other ladders when they are vertically adjacent to another (double ladders)
foreach (Item item in Item.ItemList)
{
if (!gap.IsHorizontal) continue;
//too small to walk through
if (gap.Rect.Height < 150.0f) continue;
var wayPoint = new WayPoint(
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor), SpawnType.Path, submarine, gap);
for (int dir = -1; dir <= 1; dir += 2)
{
float tolerance = gap.IsRoomToRoom ? 50.0f : outSideWaypointInterval / 2.0f;
WayPoint closest = wayPoint.FindClosest(
dir, true, new Vector2(-tolerance, tolerance),
gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
var ladders = item.GetComponent<Ladder>();
if (ladders == null) { continue; }
var wps = WayPointList.Where(wp => wp.Ladders == ladders).OrderByDescending(wp => wp.Rect.Y);
WayPoint cap = wps.First();
WayPoint above = cap.FindClosest(1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
above?.ConnectTo(cap);
WayPoint bottom = wps.Last();
WayPoint below = bottom.FindClosest(-1, horizontalSearch: false, tolerance: new Vector2(25, 50), filter: wp => wp.Ladders != null && wp.Ladders != ladders);
below?.ConnectTo(bottom);
}
foreach (Gap gap in Gap.GapList)
{
if (gap.IsHorizontal || gap.IsRoomToRoom || !gap.linkedTo.Any(l => l is Hull)) { continue; }
//too small to walk through
if (gap.Rect.Width < 100.0f) { continue; }
var wayPoint = new WayPoint(
new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2), SpawnType.Path, submarine, gap);
float tolerance = outSideWaypointInterval / 2.0f;
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
WayPoint closest = wayPoint.FindClosest(
dir, false, new Vector2(-tolerance, tolerance),
gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
if (gap.IsHorizontal)
{
wayPoint.ConnectTo(closest);
// Too small to walk through
if (gap.Rect.Height < hullMinHeight) { continue; }
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height + heightFromFloor);
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
// The closest waypoint can be quite far if the gap is at an exterior door.
Vector2 tolerance = gap.IsRoomToRoom ? new Vector2(150, 70) : new Vector2(1000, 1000);
for (int dir = -1; dir <= 1; dir += 2)
{
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: true, tolerance, gap.ConnectedDoor?.Body.FarseerBody);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
}
else
{
// Create waypoints on vertical gaps on the outer walls, also hatches.
if (gap.IsRoomToRoom || gap.linkedTo.None(l => l is Hull)) { continue; }
// Too small to swim through
if (gap.Rect.Width < 50.0f) { continue; }
Vector2 pos = new Vector2(gap.Rect.Center.X, gap.Rect.Y - gap.Rect.Height / 2);
// Some hatches are created in the block above where we handle the ladder waypoints. So we need to check for duplicates.
if (WayPointList.Any(wp => wp.ConnectedGap == gap)) { continue; }
var wayPoint = new WayPoint(pos, SpawnType.Path, submarine, gap);
Hull connectedHull = (Hull)gap.linkedTo.First(l => l is Hull);
int dir = Math.Sign(connectedHull.Position.Y - gap.Position.Y);
WayPoint closest = wayPoint.FindClosest(dir, horizontalSearch: false, new Vector2(50, 100));
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
for (dir = -1; dir <= 1; dir += 2)
{
closest = wayPoint.FindClosest(dir, horizontalSearch: true, new Vector2(500, 1000), gap.ConnectedDoor?.Body.FarseerBody, filter: wp => wp.CurrentHull == null);
if (closest != null)
{
wayPoint.ConnectTo(closest);
}
}
}
}
@@ -460,7 +615,36 @@ namespace Barotrauma
return true;
}
private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null)
private WayPoint FindClosestOutside(IEnumerable<WayPoint> waypointList, float tolerance, Body ignoredBody = null, IEnumerable<WayPoint> ignored = null, Func<WayPoint, bool> filter = null)
{
float closestDist = 0;
WayPoint closest = null;
foreach (WayPoint wp in waypointList)
{
if (wp.SpawnType != SpawnType.Path || wp == this) { continue; }
// Ignore if already linked
if (linkedTo.Contains(wp)) { continue; }
if (ignored != null && ignored.Contains(wp)) { continue; }
if (filter != null && !filter(wp)) { continue; }
float sqrDist = Vector2.DistanceSquared(Position, wp.Position);
if (closest == null || sqrDist < closestDist)
{
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false);
if (body != null && body != ignoredBody && !(body.UserData is Submarine))
{
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
{
continue;
}
}
closestDist = sqrDist;
closest = wp;
}
}
return closest;
}
private WayPoint FindClosest(int dir, bool horizontalSearch, Vector2 tolerance, Body ignoredBody = null, IEnumerable<WayPoint> ignored = null, Func<WayPoint, bool> filter = null)
{
if (dir != -1 && dir != 1) { return null; }
@@ -471,33 +655,45 @@ namespace Barotrauma
{
if (wp.SpawnType != SpawnType.Path || wp == this) { continue; }
float xDiff = wp.Position.X - Position.X;
float yDiff = wp.Position.Y - Position.Y;
float xDist = Math.Abs(xDiff);
float yDist = Math.Abs(yDiff);
if (tolerance.X < xDist) { continue; }
if (tolerance.Y < yDist) { continue; }
float dist = 0.0f;
float diff = 0.0f;
if (horizontalSearch)
{
if ((wp.Position.Y - Position.Y) < tolerance.X || (wp.Position.Y - Position.Y) > tolerance.Y) { continue; }
diff = wp.Position.X - Position.X;
dist = Math.Abs(diff) + Math.Abs(wp.Position.Y - Position.Y) / 5.0f;
diff = xDiff;
dist = xDist + yDist / 5.0f;
}
else
{
if ((wp.Position.X - Position.X) < tolerance.X || (wp.Position.X - Position.X) > tolerance.Y) { continue; }
diff = wp.Position.Y - Position.Y;
dist = Math.Abs(diff) + Math.Abs(wp.Position.X - Position.X) / 5.0f;
diff = yDiff;
dist = yDist + xDist / 5.0f;
//prefer ladder waypoints when moving vertically
if (wp.Ladders != null) { dist *= 0.5f; }
}
if (Math.Sign(diff) != dir) { continue; }
// Ignore if already linked
if (linkedTo.Contains(wp)) { continue; }
if (ignored != null && ignored.Contains(wp)) { continue; }
if (filter != null && !filter(wp)) { continue; }
if (closest == null || dist < closestDist)
{
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, true, true, false);
var body = Submarine.CheckVisibility(SimPosition, wp.SimPosition, ignoreLevel: true, ignoreSubs: true, ignoreSensors: false);
if (body != null && body != ignoredBody && !(body.UserData is Submarine))
{
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
if (body.UserData is Structure || body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall))
{
continue;
}
}
closestDist = dist;
closest = wp;
}