Unstable 0.1400.0.0

This commit is contained in:
Markus Isberg
2021-05-11 15:47:47 +03:00
parent 3f324b14e8
commit 92f0264af2
247 changed files with 8238 additions and 1911 deletions
@@ -114,6 +114,17 @@ namespace Barotrauma
public bool IsInLimbSlot(Item item, InvSlotType limbSlot)
{
if (limbSlot == (InvSlotType.LeftHand | InvSlotType.RightHand))
{
int rightHandSlot = FindLimbSlot(InvSlotType.RightHand);
int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand);
if (rightHandSlot > -1 && slots[rightHandSlot].Contains(item) &&
leftHandSlot > -1 && slots[leftHandSlot].Contains(item))
{
return true;
}
}
for (int i = 0; i < slots.Length; i++)
{
if (SlotTypes[i] == limbSlot && slots[i].Contains(item)) { return true; }
@@ -205,13 +216,41 @@ namespace Barotrauma
if (allowedSlots != null && !allowedSlots.Contains(InvSlotType.Any))
{
int slot = FindLimbSlot(allowedSlots.First());
if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item))
bool allSlotsTaken = true;
foreach (var allowedSlot in allowedSlots)
{
foreach (Item existingItem in slots[slot].Items.ToList())
if (allowedSlot == (InvSlotType.RightHand | InvSlotType.LeftHand))
{
existingItem.Drop(user);
if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); }
int rightHandSlot = FindLimbSlot(InvSlotType.RightHand);
int leftHandSlot = FindLimbSlot(InvSlotType.LeftHand);
if (rightHandSlot > -1 && slots[rightHandSlot].CanBePut(item) &&
leftHandSlot > -1 && slots[leftHandSlot].CanBePut(item))
{
allSlotsTaken = false;
break;
}
}
else
{
int slot = FindLimbSlot(allowedSlot);
if (slot > -1 && slots[slot].CanBePut(item))
{
allSlotsTaken = false;
break;
}
}
}
if (allSlotsTaken)
{
int slot = FindLimbSlot(allowedSlots.First());
if (slot > -1 && slots[slot].Items.Any(it => it != item) && slots[slot].First().AllowDroppingOnSwapWith(item))
{
foreach (Item existingItem in slots[slot].Items.ToList())
{
existingItem.Drop(user);
if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); }
}
}
}
}
@@ -304,7 +343,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && item.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i])) && slots[i].Empty())
if (allowedSlot.HasFlag(SlotTypes[i]) && item.GetComponents<Pickable>().Any(p => p.AllowedSlots.Any(s => s.HasFlag(SlotTypes[i]))) && slots[i].Empty())
{
#if CLIENT
if (PersonalSlots.HasFlag(SlotTypes[i])) { hidePersonalSlots = false; }
@@ -390,7 +429,7 @@ namespace Barotrauma
if (SlotTypes[index] == InvSlotType.Any)
{
if (!item.AllowedSlots.Contains(InvSlotType.Any)) { return false; }
if (!item.GetComponents<Pickable>().Any(p => p.AllowedSlots.Contains(InvSlotType.Any))) { return false; }
if (slots[index].Any()) { return slots[index].Contains(item); }
PutItem(item, index, user, true, createNetworkEvent);
return true;
@@ -399,20 +438,23 @@ namespace Barotrauma
InvSlotType placeToSlots = InvSlotType.None;
bool slotsFree = true;
foreach (InvSlotType allowedSlot in item.AllowedSlots)
foreach (Pickable pickable in item.GetComponents<Pickable>())
{
if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; }
#if CLIENT
if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; }
#endif
for (int i = 0; i < capacity; i++)
foreach (InvSlotType allowedSlot in pickable.AllowedSlots)
{
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item))
if (!allowedSlot.HasFlag(SlotTypes[index])) { continue; }
#if CLIENT
if (PersonalSlots.HasFlag(allowedSlot)) { hidePersonalSlots = false; }
#endif
for (int i = 0; i < capacity; i++)
{
slotsFree = false;
break;
if (allowedSlot.HasFlag(SlotTypes[i]) && slots[i].Any() && !slots[i].Contains(item))
{
slotsFree = false;
break;
}
placeToSlots = allowedSlot;
}
placeToSlots = allowedSlot;
}
}
@@ -319,6 +319,25 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Count() > 0)
{
bool inSuitableSlot = false;
for (int i = 0; i < character.Inventory.Capacity; i++)
{
if (character.Inventory.GetItemsAt(i).Contains(item))
{
if (character.Inventory.SlotTypes[i] != InvSlotType.Any &&
allowedSlots.Any(a => a.HasFlag(character.Inventory.SlotTypes[i])))
{
inSuitableSlot = true;
break;
}
}
}
if (!inSuitableSlot) { return; }
}
picker = character;
if (item.Removed)
@@ -327,6 +346,13 @@ namespace Barotrauma.Items.Components
return;
}
var wearable = item.GetComponent<Wearable>();
if (wearable != null)
{
//cannot hold and wear an item at the same time
wearable.Unequip(character);
}
if (character != null) { item.Submarine = character.Submarine; }
if (item.body == null)
{
@@ -75,6 +75,14 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
public override void Move(Vector2 amount)
{
if (trigger != null && amount.LengthSquared() > 0.00001f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
}
}
public override void Update(float deltaTime, Camera cam)
{
if (holdable != null && !holdable.Attached)
@@ -132,7 +132,12 @@ namespace Barotrauma.Items.Components
}
return false;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
return characterUsable || character == null;
}
public override void Drop(Character dropper)
{
base.Drop(dropper);
@@ -90,6 +90,24 @@ namespace Barotrauma.Items.Components
public virtual bool OnPicked(Character picker)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Count() > 0)
{
bool alreadyEquipped = false;
for (int i = 0; i < picker.Inventory.Capacity; i++)
{
if (picker.Inventory.GetItemsAt(i).Contains(item))
{
if (picker.Inventory.SlotTypes[i] != InvSlotType.Any &&
!allowedSlots.Any(a => a.HasFlag(picker.Inventory.SlotTypes[i])))
{
alreadyEquipped = true;
break;
}
}
}
if (alreadyEquipped) { return false; }
}
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
{
if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; }
@@ -151,6 +151,11 @@ namespace Barotrauma.Items.Components
return true;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
return characterUsable || character == null;
}
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
{
var containedItems = item.OwnInventory?.AllItemsMod;
@@ -660,6 +660,7 @@ namespace Barotrauma.Items.Components
/// </summary>
public virtual bool HasAccess(Character character)
{
if (item.IgnoreByAI) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.None()) { return true; }
if (character.Inventory != null)
@@ -678,7 +679,6 @@ namespace Barotrauma.Items.Components
public virtual bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
if (requiredItems.None()) { return true; }
if (!character.IsPlayer && character.Params.AI != null && character.Params.AI.Infiltrate) { return true; }
if (character.Inventory == null) { return false; }
bool hasRequiredItems = false;
bool canContinue = true;
@@ -204,6 +204,8 @@ namespace Barotrauma.Items.Components
return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null;
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
public override void Update(float deltaTime, Camera cam)
{
if (item.ParentInventory is CharacterInventory)
@@ -235,8 +237,8 @@ namespace Barotrauma.Items.Components
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(item.WorldPosition, targets);
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(item.WorldPosition, targets));
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
}
}
@@ -403,10 +405,26 @@ namespace Barotrauma.Items.Components
{
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
string[] splitIds = SpawnWithId.Split(',');
foreach (string id in splitIds)
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false);
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == id);
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
{
bool isEditor = false;
#if CLIENT
isEditor = Screen.Selected == GameMain.SubEditorScreen;
#endif
if (!isEditor && (Entity.Spawner == null || Entity.Spawner.Removed) && GameMain.NetworkMember == null)
{
var spawnedItem = new Item(prefab, Vector2.Zero, null);
Inventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots, createNetworkEvent: false);
}
else
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory, spawnIfInventoryFull: false);
}
}
}
}
}
@@ -67,6 +67,9 @@ namespace Barotrauma.Items.Components
public Character LastAIUser { get; private set; }
[Serialize(defaultValue: false, isSaveable: true)]
public bool LastUserWasPlayer { get; private set; }
private Character lastUser;
public Character LastUser
{
@@ -178,8 +181,6 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, true)]
public float AvailableFuel { get; set; }
public bool LastUserWasPlayer { get; private set; }
public Reactor(Item item, XElement element)
: base(item, element)
{
@@ -251,11 +252,13 @@ namespace Barotrauma.Items.Components
allowedFissionRate.X = Math.Min(allowedFissionRate.X, allowedFissionRate.Y - 10);
float heatAmount = GetGeneratedHeat(fissionRate);
float temperatureDiff = (heatAmount - turbineOutput) - Temperature;
Temperature += MathHelper.Clamp(Math.Sign(temperatureDiff) * 10.0f * deltaTime, -Math.Abs(temperatureDiff), Math.Abs(temperatureDiff));
//if (item.InWater && AvailableFuel < 100.0f) Temperature -= 12.0f * deltaTime;
FissionRate = MathHelper.Lerp(fissionRate, Math.Min(targetFissionRate, AvailableFuel), deltaTime);
TurbineOutput = MathHelper.Lerp(turbineOutput, targetTurbineOutput, deltaTime);
float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f);
@@ -564,6 +567,7 @@ namespace Barotrauma.Items.Components
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
character.AIController.SteeringManager.Reset();
bool shutDown = objective.Option.Equals("shutdown", StringComparison.OrdinalIgnoreCase);
IsActive = true;
@@ -598,6 +602,7 @@ namespace Barotrauma.Items.Components
void ReportFuelRodCount()
{
if (!character.IsOnPlayerTeam) { return; }
if (character.Submarine != Submarine.MainSub) { return; }
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1);
if (remainingFuelRods == 0)
{
@@ -57,6 +57,11 @@ namespace Barotrauma.Items.Components
private Submarine controlledSub;
// AI interfacing
public Vector2 AITacticalTarget { get; set; }
public float AIRamTimer { get; set; }
bool navigateTactically; // this will be removed after rewriting steering to use an enum
private bool showIceSpireWarning;
private List<Submarine> connectedSubs = new List<Submarine>();
@@ -305,7 +310,13 @@ namespace Barotrauma.Items.Components
userSkill = user.GetSkillLevel("helm") / 100.0f;
}
if (AutoPilot)
// override autopilot pathing while the AI rams, and go full speed ahead
if (AIRamTimer > 0f)
{
AIRamTimer -= deltaTime;
TargetVelocity = GetSteeringVelocity(AITacticalTarget, 0f);
}
else if (AutoPilot)
{
UpdateAutoPilot(deltaTime);
float throttle = 1.0f;
@@ -352,6 +363,14 @@ namespace Barotrauma.Items.Components
float velY = MathHelper.Lerp((neutralBallastLevel * 100 - 50) * 2, -100 * Math.Sign(targetVelocity.Y), Math.Abs(targetVelocity.Y) / 100.0f);
item.SendSignal(new Signal(velY.ToString(CultureInfo.InvariantCulture), sender: user), "velocity_y_out");
// if our tactical AI pilot has left, revert back to maintaining position
if (navigateTactically && (user == null || user.SelectedConstruction != item))
{
navigateTactically = false;
AIRamTimer = 0f;
SetMaintainPosition();
}
}
private void IncreaseSkillLevel(Character user, float deltaTime)
@@ -580,13 +599,18 @@ namespace Barotrauma.Items.Components
}
Vector2 target;
if (LevelEndSelected)
if (navigateTactically)
{
target = ConvertUnits.ToSimUnits(Level.Loaded.EndPosition);
target = ConvertUnits.ToSimUnits(AITacticalTarget);
}
else if (LevelEndSelected)
{
target = ConvertUnits.ToSimUnits(Level.Loaded.EndExitPosition);
}
else
{
target = ConvertUnits.ToSimUnits(Level.Loaded.StartPosition);
target = ConvertUnits.ToSimUnits(Level.Loaded.StartExitPosition);
}
steeringPath = pathFinder.FindPath(ConvertUnits.ToSimUnits(controlledSub == null ? item.WorldPosition : controlledSub.WorldPosition), target, errorMsgStr: "(Autopilot, target: " + target + ")");
}
@@ -597,6 +621,7 @@ namespace Barotrauma.Items.Components
MaintainPos = false;
posToMaintain = null;
LevelEndSelected = false;
navigateTactically = false;
if (!LevelStartSelected)
{
LevelStartSelected = true;
@@ -610,6 +635,7 @@ namespace Barotrauma.Items.Components
MaintainPos = false;
posToMaintain = null;
LevelStartSelected = false;
navigateTactically = false;
if (!LevelEndSelected)
{
LevelEndSelected = true;
@@ -617,6 +643,36 @@ namespace Barotrauma.Items.Components
}
}
private void SetDestinationTactical()
{
AutoPilot = true;
MaintainPos = false;
posToMaintain = null;
LevelStartSelected = false;
LevelEndSelected = false;
if (!navigateTactically)
{
navigateTactically = true;
UpdatePath();
}
}
private void SetMaintainPosition()
{
if (!MaintainPos)
{
unsentChanges = true;
MaintainPos = true;
}
if (!posToMaintain.HasValue)
{
unsentChanges = true;
posToMaintain = controlledSub != null ?
controlledSub.WorldPosition :
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
}
}
/// <summary>
/// Get optimal velocity for moving towards a position
/// </summary>
@@ -640,6 +696,7 @@ namespace Barotrauma.Items.Components
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
character.AIController.SteeringManager.Reset();
if (objective.Override)
{
if (user != character && user != null && user.SelectedConstruction == item && character.IsOnPlayerTeam)
@@ -659,18 +716,7 @@ namespace Barotrauma.Items.Components
case "maintainposition":
if (objective.Override)
{
if (!MaintainPos)
{
unsentChanges = true;
MaintainPos = true;
}
if (!posToMaintain.HasValue)
{
unsentChanges = true;
posToMaintain = controlledSub != null ?
controlledSub.WorldPosition :
item.Submarine == null ? item.WorldPosition : item.Submarine.WorldPosition;
}
SetMaintainPosition();
}
break;
case "navigateback":
@@ -681,7 +727,7 @@ namespace Barotrauma.Items.Components
}
if (objective.Override)
{
if (MaintainPos || LevelEndSelected || !LevelStartSelected)
if (MaintainPos || LevelEndSelected || !LevelStartSelected || navigateTactically)
{
unsentChanges = true;
}
@@ -696,13 +742,28 @@ namespace Barotrauma.Items.Components
}
if (objective.Override)
{
if (MaintainPos || !LevelEndSelected || LevelStartSelected)
if (MaintainPos || !LevelEndSelected || LevelStartSelected || navigateTactically)
{
unsentChanges = true;
}
SetDestinationLevelEnd();
}
break;
case "navigatetactical":
if (Level.IsLoadedOutpost) { break; }
if (DockingSources.Any(d => d.Docked))
{
item.SendSignal("1", "toggle_docking");
}
if (objective.Override)
{
if (MaintainPos || LevelEndSelected || LevelStartSelected || !navigateTactically)
{
unsentChanges = true;
}
SetDestinationTactical();
}
break;
}
sonar?.AIOperate(deltaTime, character, objective);
if (!MaintainPos && showIceSpireWarning && character.IsOnPlayerTeam)
@@ -137,16 +137,9 @@ namespace Barotrauma.Items.Components
return true;
}
if (HasAnyFinishedGrowing())
{
Msg = MsgHarvest;
ParseMsg();
return true;
}
Msg = string.Empty;
Msg = MsgHarvest;
ParseMsg();
return false;
return true;
}
public override bool Pick(Character character)
@@ -199,12 +192,13 @@ namespace Barotrauma.Items.Components
{
Debug.Assert(container != null, "Tried to harvest a planter without an item container.");
bool anyDecayed = GrowableSeeds.Any(s => s is { } seed && (seed.Decayed || seed.FullyGrown));
for (var i = 0; i < GrowableSeeds.Length; i++)
{
Growable? seed = GrowableSeeds[i];
if (seed == null) { continue; }
if (seed.Decayed || seed.FullyGrown)
if (!anyDecayed || seed.Decayed || seed.FullyGrown)
{
container?.Inventory.RemoveItem(seed.Item);
Entity.Spawner?.AddToRemoveQueue(seed.Item);
@@ -20,7 +20,6 @@ namespace Barotrauma.Items.Components
public Vector2 Point;
public Vector2 Normal;
public float Fraction;
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction)
{
Fixture = fixture;
@@ -81,7 +80,11 @@ namespace Barotrauma.Items.Components
[Serialize(10.0f, false, description: "The impulse applied to the physics body of the item when it's launched. Higher values make the projectile faster.")]
public float LaunchImpulse { get; set; }
[Serialize(0.0f, false, description: "The random percentage modifier used to add variance to the launch impulse.")]
public float ImpulseSpread { get; set; }
[Serialize(0.0f, false, description: "The rotation of the item relative to the rotation of the weapon when launched (in degrees).")]
public float LaunchRotation
{
get { return MathHelper.ToDegrees(LaunchRotationRadians); }
@@ -189,7 +192,9 @@ namespace Barotrauma.Items.Components
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
Attack = new Attack(subElement, item.Name + ", Projectile");
}
InitProjSpecific(element);
}
partial void InitProjSpecific(XElement element);
public override void OnItemLoaded()
{
@@ -269,6 +274,7 @@ namespace Barotrauma.Items.Components
if (Hitscan)
{
Vector2 prevSimpos = item.SimPosition;
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
DoHitscan(launchDir);
if (i < HitScanCount - 1)
{
@@ -277,7 +283,8 @@ namespace Barotrauma.Items.Components
}
else
{
DoLaunch(launchDir * LaunchImpulse * item.body.Mass);
float modifiedLaunchImpulse = LaunchImpulse * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
DoLaunch(launchDir * modifiedLaunchImpulse * item.body.Mass);
}
}
User = character;
@@ -331,7 +338,14 @@ namespace Barotrauma.Items.Components
IsActive = true;
Vector2 rayStart = simPositon;
Vector2 rayEnd = simPositon + dir * 500.0f;
Vector2 rayEnd = rayStart + dir * 500.0f;
Vector2 rayStartWorld = item.WorldPosition;
float worldDist = 1000.0f;
#if CLIENT
worldDist = Screen.Selected?.Cam?.WorldView.Width ?? GameMain.GraphicsWidth;
#endif
Vector2 rayEndWorld = rayStartWorld + dir * worldDist;
List<HitscanResult> hits = new List<HitscanResult>();
@@ -375,6 +389,7 @@ namespace Barotrauma.Items.Components
item.SetTransform(h.Point, rotation);
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{
LaunchProjSpecific(rayStartWorld, item.WorldPosition);
hitSomething = true;
break;
}
@@ -383,6 +398,8 @@ namespace Barotrauma.Items.Components
//the raycast didn't hit anything -> the projectile flew somewhere outside the level and is permanently lost
if (!hitSomething)
{
item.body.SetTransformIgnoreContacts(item.body.SimPosition, rotation);
LaunchProjSpecific(rayStartWorld, rayEndWorld);
if (Entity.Spawner == null)
{
item.Remove();
@@ -605,6 +622,8 @@ namespace Barotrauma.Items.Components
}
}
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private bool HandleProjectileCollision(Fixture target, Vector2 collisionNormal, Vector2 velocity)
{
if (User != null && User.Removed) { User = null; }
@@ -615,6 +634,9 @@ namespace Barotrauma.Items.Components
return false;
}
float projectileNewSpeed = 0.5f;
float projectileDeflectedNewSpeed = 0.1f;
AttackResult attackResult = new AttackResult();
Character character = null;
if (target.Body.UserData is Submarine submarine)
@@ -626,6 +648,12 @@ namespace Barotrauma.Items.Components
}
else if (target.Body.UserData is Limb limb)
{
// when hitting limbs with piercing ammo, don't lose as much speed
if (MaxTargetsToHit > 1)
{
projectileNewSpeed = 1f;
projectileDeflectedNewSpeed = 0.8f;
}
//severed limbs don't deactivate the projectile (but may still slow it down enough to make it inactive)
if (limb.IsSevered) { return true; }
if (limb.character == null || limb.character.Removed) { return false; }
@@ -690,8 +718,8 @@ namespace Barotrauma.Items.Components
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(targetLimb.WorldPosition, targets);
targets.Clear();
targets.AddRange(effect.GetNearbyTargets(targetLimb.WorldPosition, targets));
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
}
@@ -731,7 +759,7 @@ namespace Barotrauma.Items.Components
if (attackResult.AppliedDamageModifiers != null &&
attackResult.AppliedDamageModifiers.Any(dm => dm.DeflectProjectiles))
{
item.body.LinearVelocity *= 0.1f;
item.body.LinearVelocity *= projectileDeflectedNewSpeed;
}
else if (Vector2.Dot(velocity, collisionNormal) < 0.0f && hits.Count() >= MaxTargetsToHit &&
target.Body.Mass > item.body.Mass * 0.5f &&
@@ -761,13 +789,13 @@ namespace Barotrauma.Items.Components
item.CreateServerEvent(this);
}
#endif
item.body.LinearVelocity *= 0.5f;
item.body.LinearVelocity *= projectileNewSpeed;
return Hitscan;
}
else
{
item.body.LinearVelocity *= 0.5f;
item.body.LinearVelocity *= projectileNewSpeed;
}
var containedItems = item.OwnInventory?.AllItems;
@@ -876,5 +904,6 @@ namespace Barotrauma.Items.Components
}
}
partial void LaunchProjSpecific(Vector2 startLocation, Vector2 endLocation);
}
}
@@ -10,7 +10,8 @@ namespace Barotrauma.Items.Components
{
partial class Rope : ItemComponent, IServerSerializable
{
private Item source, target;
private ISpatialEntity source;
private Item target;
private float snapTimer;
private const float SnapAnimDuration = 1.0f;
@@ -85,7 +86,7 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element);
public void Attach(Item source, Item target)
public void Attach(ISpatialEntity source, Item target)
{
System.Diagnostics.Debug.Assert(source != null);
System.Diagnostics.Debug.Assert(target != null);
@@ -97,7 +98,8 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (source == null || source.Removed || target == null || target.Removed)
if (source == null || target == null || target.Removed ||
(source is Entity sourceEntity && sourceEntity.Removed))
{
IsActive = false;
return;
@@ -224,32 +226,40 @@ namespace Barotrauma.Items.Components
}
}
private PhysicsBody GetBodyToPull(Item target)
private PhysicsBody GetBodyToPull(ISpatialEntity target)
{
if (target.ParentInventory is CharacterInventory characterInventory &&
characterInventory.Owner is Character ownerCharacter)
if (target is Item targetItem)
{
if (ownerCharacter.Removed) { return null; }
return ownerCharacter.AnimController.Collider;
if (targetItem.ParentInventory is CharacterInventory characterInventory &&
characterInventory.Owner is Character ownerCharacter)
{
if (ownerCharacter.Removed) { return null; }
return ownerCharacter.AnimController.Collider;
}
var projectile = targetItem.GetComponent<Projectile>();
if (projectile != null)
{
if (projectile.StickTarget?.UserData is Structure structure)
{
return structure.Submarine?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Submarine sub)
{
return sub?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Character character)
{
return character.AnimController.Collider;
}
return null;
}
if (targetItem.body != null) { return targetItem.body; }
}
var projectile = target.GetComponent<Projectile>();
if (projectile != null)
else if (target is Limb targetLimb)
{
if (projectile.StickTarget?.UserData is Structure structure)
{
return structure.Submarine?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Submarine sub)
{
return sub?.PhysicsBody;
}
else if (projectile.StickTarget?.UserData is Character character)
{
return character.AnimController.Collider;
}
return null;
return targetLimb.body;
}
if (target.body != null) { return target.body; }
return null;
}
}
@@ -19,7 +19,8 @@ namespace Barotrauma.Items.Components
{
Any,
Human,
Monster
Monster,
Wall
}
[Serialize(false, false, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
@@ -153,7 +154,7 @@ namespace Barotrauma.Items.Components
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(new Signal(signalOut, 1), "state_out"); }
updateTimer -= deltaTime;
if (updateTimer > 0.0f) return;
if (updateTimer > 0.0f) { return; }
MotionDetected = false;
updateTimer = UpdateInterval;
@@ -171,6 +172,30 @@ namespace Barotrauma.Items.Components
float broadRangeX = Math.Max(rangeX * 2, 500);
float broadRangeY = Math.Max(rangeY * 2, 500);
if (item.CurrentHull == null && item.Submarine != null && Level.Loaded != null &&
(Target == TargetType.Wall || Target == TargetType.Any) &&
(Math.Abs(item.Submarine.Velocity.X) > MinimumVelocity || Math.Abs(item.Submarine.Velocity.Y) > MinimumVelocity))
{
var cells = Level.Loaded.GetCells(item.WorldPosition, 1);
foreach (var cell in cells)
{
if (cell.IsPointInside(item.WorldPosition))
{
MotionDetected = true;
return;
}
foreach (var edge in cell.Edges)
{
var closestPoint = MathUtils.GetClosestPointOnLineSegment(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, item.WorldPosition);
if (Math.Abs(closestPoint.X - item.WorldPosition.X) < rangeX && Math.Abs(closestPoint.Y - item.WorldPosition.Y) < rangeY)
{
MotionDetected = true;
return;
}
}
}
}
foreach (Character c in Character.CharacterList)
{
if (IgnoreDead && c.IsDead) { continue; }
@@ -187,6 +212,8 @@ namespace Barotrauma.Items.Components
case TargetType.Monster:
if (c.IsHuman || c.IsPet) { continue; }
break;
case TargetType.Wall:
break;
}
//do a rough check based on the position of the character's collider first
@@ -203,7 +230,7 @@ namespace Barotrauma.Items.Components
if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
{
MotionDetected = true;
break;
return;
}
}
}
@@ -58,7 +58,8 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " +
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat)]
[Serialize(false, false, description: "If enabled, any signals received from another chat-linked wifi component are displayed " +
"as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)]
public bool LinkToChat
{
@@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components
partial class Turret : Powered, IDrawableComponent, IServerSerializable
{
private Sprite barrelSprite, railSprite;
private List<Tuple<Sprite, Vector2>> chargeSprites = new List<Tuple<Sprite, Vector2>>();
private List<Sprite> spinningBarrelSprites = new List<Sprite>();
private Vector2 barrelPos;
private Vector2 transformedBarrelPos;
@@ -35,6 +37,20 @@ namespace Barotrauma.Items.Components
private int failedLaunchAttempts;
private float currentChargeTime;
private bool tryingToCharge;
private enum ChargingState
{
Inactive,
WindingUp,
WindingDown,
}
private ChargingState currentChargingState;
private float currentBarrelSpin = 0f;
private readonly List<Item> activeProjectiles = new List<Item>();
public IEnumerable<Item> ActiveProjectiles => activeProjectiles;
@@ -42,6 +58,12 @@ namespace Barotrauma.Items.Components
private float resetUserTimer;
private float aiTargetingGraceTimer;
private float aiFindTargetTimer;
private Character currentTarget;
const float aiFindTargetInterval = 5.0f;
public float Rotation
{
get { return rotation; }
@@ -61,6 +83,12 @@ namespace Barotrauma.Items.Components
}
}
[Serialize("0,0", false, description: "The projectile launching location relative to transformed barrel position (in pixels).")]
public Vector2 FiringOffset
{
get;
set;
}
public Vector2 TransformedBarrelPos
{
get
@@ -198,6 +226,20 @@ namespace Barotrauma.Items.Components
private set;
}
[Serialize(1.0f, false, description: "How fast the turret can rotate while firing (for charged weapons).")]
public float FiringRotationSpeedModifier
{
get;
private set;
}
[Serialize(false, true, description: "Whether the turret should always charge-up fully to shoot.")]
public bool SingleChargedShot
{
get;
private set;
}
private float prevScale;
float prevBaseRotation;
[Serialize(0.0f, true, description: "The angle of the turret's base in degrees.", alwaysUseInstanceValues: true)]
@@ -225,6 +267,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(0f, true, description: "The time required for a charge-type turret to charge up before able to fire.")]
public float MaxChargeTime
{
get;
private set;
}
public Turret(Item item, XElement element)
: base(item, element)
{
@@ -240,6 +289,16 @@ namespace Barotrauma.Items.Components
case "railsprite":
railSprite = new Sprite(subElement);
break;
case "chargesprite":
chargeSprites.Add(new Tuple<Sprite, Vector2>(new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero)));
break;
case "spinningbarrelsprite":
int spriteCount = subElement.GetAttributeInt("spriteamount", 1);
for (int i = 0; i < spriteCount; i++)
{
spinningBarrelSprites.Add(new Sprite(subElement));
}
break;
}
}
item.IsShootable = true;
@@ -311,6 +370,35 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
float previousChargeTime = currentChargeTime;
if (SingleChargedShot && reload > 0f)
{
// single charged shot guns will decharge after firing
// for cosmetic reasons, this is done by lerping in half the reload time
currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f));
}
else
{
float chargeDeltaTime = tryingToCharge ? deltaTime : -deltaTime;
currentChargeTime = Math.Clamp(currentChargeTime + chargeDeltaTime, 0f, MaxChargeTime);
}
tryingToCharge = false;
if (currentChargeTime == 0f)
{
currentChargingState = ChargingState.Inactive;
}
else if (currentChargeTime < previousChargeTime)
{
currentChargingState = ChargingState.WindingDown;
}
else
{
// if we are charging up or at maxed charge, remain winding up
currentChargingState = ChargingState.WindingUp;
}
UpdateProjSpecific(deltaTime);
if (MathUtils.NearlyEqual(minRotation, maxRotation))
@@ -333,6 +421,10 @@ namespace Barotrauma.Items.Components
float springStiffness = MathHelper.Lerp(SpringStiffnessLowSkill, SpringStiffnessHighSkill, degreeOfSuccess);
float springDamping = MathHelper.Lerp(SpringDampingLowSkill, SpringDampingHighSkill, degreeOfSuccess);
float rotationSpeed = MathHelper.Lerp(RotationSpeedLowSkill, RotationSpeedHighSkill, degreeOfSuccess);
if (MaxChargeTime > 0)
{
rotationSpeed *= MathHelper.Lerp(1f, FiringRotationSpeedModifier, MathUtils.EaseIn(currentChargeTime / MaxChargeTime));
}
// Do not increase the weapons skill when operating a turret in an outpost level
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedOutpost))
@@ -384,6 +476,15 @@ namespace Barotrauma.Items.Components
angularVelocity *= -0.5f;
}
if (aiTargetingGraceTimer > 0f)
{
aiTargetingGraceTimer -= deltaTime;
}
if (aiFindTargetTimer > 0.0f)
{
aiFindTargetTimer -= deltaTime;
}
UpdateLightComponent();
}
@@ -403,10 +504,18 @@ namespace Barotrauma.Items.Components
return TryLaunch(deltaTime, character);
}
public bool HasPowerToShoot()
{
return GetAvailableBatteryPower() >= powerConsumption;
}
private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
{
tryingToCharge = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (currentChargeTime < MaxChargeTime) { return false; }
if (reload > 0.0f) { return false; }
if (MaxActiveProjectiles >= 0)
@@ -420,7 +529,7 @@ namespace Barotrauma.Items.Components
if (!ignorePower)
{
if (GetAvailableBatteryPower() < powerConsumption)
if (!HasPowerToShoot())
{
#if CLIENT
if (!flashLowPower && character != null && character == Character.Controlled)
@@ -434,13 +543,17 @@ namespace Barotrauma.Items.Components
}
Projectile launchedProjectile = null;
bool loaderBroken = false;
for (int i = 0; i < ProjectileCount; i++)
{
var projectiles = GetLoadedProjectiles(true);
var projectiles = GetLoadedProjectiles();
if (projectiles.Any())
{
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer?.Item != item) { projectileContainer?.Item.Use(deltaTime, null); }
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer?.Item.Use(deltaTime, null);
}
}
else
{
@@ -449,11 +562,16 @@ namespace Barotrauma.Items.Components
//use linked projectile containers in case they have to react to the turret being launched somehow
//(play a sound, spawn more projectiles)
if (!(e is Item linkedItem)) { continue; }
if (linkedItem.Condition <= 0.0f)
{
loaderBroken = true;
continue;
}
ItemContainer projectileContainer = linkedItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
{
linkedItem.Use(deltaTime, null);
projectiles = GetLoadedProjectiles(true);
projectiles = GetLoadedProjectiles();
if (projectiles.Any()) { break; }
}
}
@@ -465,9 +583,16 @@ namespace Barotrauma.Items.Components
// -> attempt to launch the gun multiple times before showing the "no ammo" flash
failedLaunchAttempts++;
#if CLIENT
if (!flashNoAmmo && character != null && character == Character.Controlled && failedLaunchAttempts > 20)
if (!flashNoAmmo && !flashLoaderBroken && character != null && character == Character.Controlled && failedLaunchAttempts > 20)
{
flashNoAmmo = true;
if (loaderBroken)
{
flashLoaderBroken = true;
}
else
{
flashNoAmmo = true;
}
failedLaunchAttempts = 0;
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
@@ -475,7 +600,6 @@ namespace Barotrauma.Items.Components
return false;
}
failedLaunchAttempts = 0;
launchedProjectile = projectiles.FirstOrDefault();
if (!ignorePower)
{
var batteries = item.GetConnectedComponents<PowerContainer>();
@@ -496,6 +620,7 @@ namespace Barotrauma.Items.Components
}
}
launchedProjectile = projectiles.FirstOrDefault();
if (launchedProjectile?.Item.Container != null)
{
var repairable = launchedProjectile?.Item.Container.GetComponent<Repairable>();
@@ -507,7 +632,17 @@ namespace Barotrauma.Items.Components
if (launchedProjectile != null || LaunchWithoutProjectile)
{
Launch(launchedProjectile?.Item, character);
if (projectiles.Any())
{
foreach (Projectile projectile in projectiles)
{
Launch(projectile.Item, character);
}
}
else
{
Launch(null, character);
}
if (item.AiTarget != null)
{
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
@@ -550,10 +685,10 @@ namespace Barotrauma.Items.Components
projectile.body.ResetDynamics();
projectile.body.Enabled = true;
}
float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f);
projectile.SetTransform(
ConvertUnits.ToSimUnits(new Vector2(item.WorldRect.X + transformedBarrelPos.X, item.WorldRect.Y - transformedBarrelPos.Y)),
ConvertUnits.ToSimUnits(GetRelativeFiringPosition()),
-(launchRotation ?? rotation) + spread);
projectile.UpdateTransform();
projectile.Submarine = projectile.body?.Submarine;
@@ -561,7 +696,7 @@ namespace Barotrauma.Items.Components
Projectile projectileComponent = projectile.GetComponent<Projectile>();
if (projectileComponent != null)
{
projectileComponent.Use((float)Timing.Step);
projectileComponent.Use();
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
@@ -780,7 +915,6 @@ namespace Barotrauma.Items.Components
TryLaunch(deltaTime, ignorePower: true);
}
private bool outOfAmmo;
public override bool AIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (character.AIController.SelectedAiTarget?.Entity is Character previousTarget &&
@@ -790,7 +924,7 @@ namespace Barotrauma.Items.Components
character.AIController.SelectTarget(null);
}
if (GetAvailableBatteryPower() < powerConsumption)
if (!HasPowerToShoot())
{
var batteries = item.GetConnectedComponents<PowerContainer>();
@@ -868,11 +1002,12 @@ namespace Barotrauma.Items.Components
void CheckRemainingAmmo()
{
if (!character.IsOnPlayerTeam) { return; }
string ammoType = container.Item.HasTag("railgunammosource") ? "railgunammo" : container.Item.HasTag("coilgunammosource") ? "coilgunammo" : "turretammo";
if (character.Submarine != Submarine.MainSub) { return; }
string ammoType = container.ContainableItems.First().Identifiers.FirstOrDefault() ?? "ammobox";
int remainingAmmo = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(ammoType) && i.Condition > 1);
if (remainingAmmo == 0)
{
character.Speak(TextManager.Get($"DialogOutOf{ammoType}"), null, 0.0f, "outofammo", 30.0f);
character.Speak(TextManager.Get($"DialogOutOf{ammoType}", fallBackTag: "DialogOutOfTurretAmmo"), null, 0.0f, "outofammo", 30.0f);
}
else if (remainingAmmo < 3)
{
@@ -891,24 +1026,48 @@ namespace Barotrauma.Items.Components
Vector2? targetPos = null;
float maxDistance = 10000;
float shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
float closestDistance = maxDistance * maxDistance;
foreach (Character enemy in Character.CharacterList)
// use full range only if we're actively firing
if (aiTargetingGraceTimer <= 0f)
{
// Ignore dead, friendly, and those that are inside the same sub
if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; }
// Don't aim monsters that are inside a submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist < shootDistance * shootDistance)
shootDistance *= 0.75f;
}
float closestDistance = maxDistance * maxDistance;
if (currentTarget != null)
{
if (currentTarget.Removed || currentTarget.IsDead)
{
// Only check the angle to targets that are close enough to be shot at
// We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at.
if (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
currentTarget = null;
}
closestEnemy = enemy;
closestDistance = dist;
}
if (aiFindTargetTimer <= 0.0f || currentTarget == null)
{
foreach (Character enemy in Character.CharacterList)
{
// Ignore dead, friendly, and those that are inside the same sub
if (enemy.IsDead || !enemy.Enabled || enemy.Submarine == character.Submarine) { continue; }
// Don't aim monsters that are inside a submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist < shootDistance * shootDistance)
{
// Only check the angle to targets that are close enough to be shot at
// We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at.
if (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
}
closestEnemy = enemy;
closestDistance = dist;
}
currentTarget = closestEnemy;
aiFindTargetTimer = aiFindTargetInterval;
}
else
{
closestEnemy = currentTarget;
}
if (closestEnemy != null)
@@ -938,6 +1097,7 @@ namespace Barotrauma.Items.Components
else if (item.Submarine != null && Level.Loaded != null)
{
// Check ice spires
shootDistance = AIRange * item.OffsetOnSelectedMultiplier;
closestDistance = shootDistance;
foreach (var wall in Level.Loaded.ExtraWalls)
{
@@ -990,6 +1150,8 @@ namespace Barotrauma.Items.Components
}
if (targetPos == null) { return false; }
// Force the highest priority so that we don't change the objective while targeting enemies.
objective.ForceHighestPriority = true;
if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget)
{
@@ -1032,7 +1194,14 @@ namespace Barotrauma.Items.Components
float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition);
float turretAngle = -rotation;
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return false; }
float maxAngleError = 0.15f;
if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f)
{
//larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target)
maxAngleError *= 2.0f;
}
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; }
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
@@ -1088,10 +1257,22 @@ namespace Barotrauma.Items.Components
{
character.Speak(TextManager.Get("DialogFireTurret"), null, 0.0f, "fireturret", 10.0f);
}
aiTargetingGraceTimer = 5f;
character.SetInput(InputType.Shoot, true, true);
return false;
}
private Vector2 GetRelativeFiringPosition(bool useOffset = true)
{
// i don't feel great about this method, should be evaluated again
Vector2 transformedFiringOffset = Vector2.Zero;
if (useOffset)
{
transformedFiringOffset = MathUtils.RotatePoint(new Vector2(-FiringOffset.Y, -FiringOffset.X) * item.Scale, -rotation);
}
return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y);
}
private bool CheckTurretAngle(float angle)
{
float midRotation = (minRotation + maxRotation) / 2.0f;
@@ -1116,22 +1297,28 @@ namespace Barotrauma.Items.Components
#endif
}
private List<Projectile> GetLoadedProjectiles(bool returnFirst = false)
private List<Projectile> GetLoadedProjectiles()
{
List<Projectile> projectiles = new List<Projectile>();
//check the item itself first
CheckProjectileContainer(item, projectiles, returnFirst);
// check the item itself first
CheckProjectileContainer(item, projectiles, out bool _);
foreach (MapEntity e in item.linkedTo)
{
if (e is Item projectileContainer) { CheckProjectileContainer(projectileContainer, projectiles, returnFirst); }
if (returnFirst && projectiles.Any()) { return projectiles; }
if (e is Item projectileContainer)
{
CheckProjectileContainer(projectileContainer, projectiles, out bool stopSearching);
if (projectiles.Any() || stopSearching) { return projectiles; }
}
}
return projectiles;
}
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, bool returnFirst)
private void CheckProjectileContainer(Item projectileContainer, List<Projectile> projectiles, out bool stopSearching)
{
stopSearching = false;
if (projectileContainer.Condition <= 0.0f) { return; }
var containedItems = projectileContainer.ContainedItems;
if (containedItems == null) { return; }
@@ -1141,7 +1328,7 @@ namespace Barotrauma.Items.Components
if (projectileComponent != null && projectileComponent.Item.body != null)
{
projectiles.Add(projectileComponent);
if (returnFirst) { return; }
return;
}
else
{
@@ -1152,9 +1339,15 @@ namespace Barotrauma.Items.Components
if (projectileComponent != null && projectileComponent.Item.body != null)
{
projectiles.Add(projectileComponent);
if (returnFirst) { return; }
}
}
// in the case that we found a container that still has condition/ammo left,
// return and inform GetLoadedProjectiles to stop searching past this point (even if no projectiles were not found)
if (containedItem.Condition > 0.0f || projectiles.Any())
{
stopSearching = true;
return;
}
}
}
}
@@ -1267,7 +1460,7 @@ namespace Barotrauma.Items.Components
{
if (extraData.Length > 2)
{
msg.Write(!(extraData[2] is Item item) || item.Removed ? ushort.MaxValue : item.ID);
msg.Write(!(extraData[2] is Item item) ? ushort.MaxValue : item.ID);
msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16);
}
else
@@ -51,6 +51,9 @@ namespace Barotrauma
public bool InheritTextureScale { get; private set; }
public bool InheritOrigin { get; private set; }
public bool InheritSourceRect { get; private set; }
public float Scale { get; private set; }
public LimbType DepthLimb { get; private set; }
private Wearable _wearableComponent;
public Wearable WearableComponent
@@ -175,6 +178,7 @@ namespace Barotrauma
InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false);
DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true);
Sound = SourceElement.GetAttributeString("sound", "");
Scale = SourceElement.GetAttributeFloat("scale", 1.0f);
var index = SourceElement.GetAttributePoint("sheetindex", new Point(-1, -1));
if (index.X > -1 && index.Y > -1)
{
@@ -313,6 +317,11 @@ namespace Barotrauma.Items.Components
public override void Equip(Character character)
{
foreach (var allowedSlot in allowedSlots)
{
if (allowedSlot != InvSlotType.Any && !character.Inventory.IsInLimbSlot(item, allowedSlot)) { return; }
}
picker = character;
for (int i = 0; i < wearableSprites.Length; i++ )
{
@@ -670,7 +670,11 @@ namespace Barotrauma
if (!swapSuccessful && existingItems.Count == 1 && existingItems[0].AllowDroppingOnSwapWith(item))
{
existingItems[0].Drop(user, createNetworkEvent);
if (!(existingItems[0].Container?.ParentInventory is CharacterInventory characterInv) ||
!characterInv.TryPutItem(existingItems[0], user, new List<InvSlotType>() { InvSlotType.Any }))
{
existingItems[0].Drop(user, createNetworkEvent);
}
swapSuccessful = stackedItems.Distinct().Any(stackedItem => TryPutItem(stackedItem, index, false, false, user, createNetworkEvent));
#if CLIENT
if (swapSuccessful)
@@ -882,7 +886,7 @@ namespace Barotrauma
}
}
}
/// <summary>
/// Deletes all items inside the inventory (and also recursively all items inside the items)
/// </summary>
@@ -6,7 +6,6 @@ using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
@@ -19,7 +18,6 @@ using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerSerializable, IClientSerializable
{
public static List<Item> ItemList = new List<Item>();
@@ -41,7 +39,24 @@ namespace Barotrauma
ParentRuin = currentHull?.ParentRuin;
}
}
private CampaignMode.InteractionType campaignInteractionType = CampaignMode.InteractionType.None;
public CampaignMode.InteractionType CampaignInteractionType
{
get { return campaignInteractionType; }
set
{
if (campaignInteractionType != value)
{
campaignInteractionType = value;
AssignCampaignInteractionTypeProjSpecific(campaignInteractionType);
}
}
}
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType);
public bool Visible = true;
#if CLIENT
@@ -109,7 +124,9 @@ namespace Barotrauma
foreach (ItemComponent component in components)
{
if (!component.AllowInGameEditing) { continue; }
if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any()))
if (component.SerializableProperties.Values.Any(p => p.Attributes.OfType<InGameEditable>().Any())
|| component.SerializableProperties.Values.Any(p => p.Attributes.OfType<ConditionallyEditable>().Any(a => a.IsEditable()))
)
{
hasInGameEditableProperties = true;
break;
@@ -609,12 +626,12 @@ namespace Barotrauma
}
//which type of inventory slots (head, torso, any, etc) the item can be placed in
private readonly HashSet<InvSlotType> allowedSlots = new HashSet<InvSlotType>();
public IEnumerable<InvSlotType> AllowedSlots
{
get
{
Pickable p = GetComponent<Pickable>();
return (p == null) ? InvSlotType.Any.ToEnumerable() : p.AllowedSlots;
return allowedSlots;
}
}
@@ -663,6 +680,10 @@ namespace Barotrauma
public BallastFloraBranch Infector { get; set; }
public ItemPrefab PendingItemSwap { get; set; }
public readonly HashSet<ItemPrefab> AvailableSwaps = new HashSet<ItemPrefab>();
public override string ToString()
{
#if CLIENT
@@ -765,6 +786,7 @@ namespace Barotrauma
case "deconstruct":
case "brokensprite":
case "decorativesprite":
case "upgradepreviewsprite":
case "price":
case "levelcommonness":
case "suitabletreatment":
@@ -779,6 +801,7 @@ namespace Barotrauma
case "minimapicon":
case "infectedsprite":
case "damagedinfectedsprite":
case "swappableitem":
break;
case "staticbody":
StaticBodyConfig = subElement;
@@ -805,7 +828,15 @@ namespace Barotrauma
hasStatusEffectsOfType = new bool[Enum.GetValues(typeof(ActionType)).Length];
foreach (ItemComponent ic in components)
{
if (ic.statusEffectLists == null) continue;
if (ic is Pickable pickable)
{
foreach (var allowedSlot in pickable.AllowedSlots)
{
allowedSlots.Add(allowedSlot);
}
}
if (ic.statusEffectLists == null) { continue; }
if (statusEffectLists == null)
{
@@ -1034,14 +1065,20 @@ namespace Barotrauma
{
return (T)component;
}
if (typeof(T) == typeof(ItemComponent))
{
return (T)components.FirstOrDefault();
}
return default;
}
public IEnumerable<T> GetComponents<T>()
{
if (typeof(T) == typeof(ItemComponent))
{
return components.Cast<T>();
}
if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty<T>(); }
return components.Where(c => c is T).Cast<T>();
}
@@ -1382,8 +1419,11 @@ namespace Barotrauma
if (effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters) || effect.HasTargetType(StatusEffect.TargetType.NearbyItems))
{
effect.GetNearbyTargets(WorldPosition, targets);
if (targets.Count > 0) { hasTargets = true; }
targets.AddRange(effect.GetNearbyTargets(WorldPosition, targets));
if (targets.Count > 0)
{
hasTargets = true;
}
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget) && useTarget is ISerializableEntity serializableTarget)
@@ -2064,7 +2104,7 @@ namespace Barotrauma
if (!ic.HasRequiredSkills(picker, out Skill tempRequiredSkill)) { hasRequiredSkills = false; skillMultiplier = ic.GetSkillMultiplier(); }
showUiMsg = picker == Character.Controlled && Screen.Selected != GameMain.SubEditorScreen;
#endif
if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) continue;
if (!ignoreRequiredItems && !ic.HasRequiredItems(picker, showUiMsg)) { continue; }
if ((ic.CanBePicked && pickHit && ic.Pick(picker)) ||
(ic.CanBeSelected && selectHit && ic.Select(picker)))
{
@@ -2074,11 +2114,11 @@ namespace Barotrauma
if (picker == Character.Controlled) { GUI.ForceMouseOn(null); }
if (tempRequiredSkill != null) { requiredSkill = tempRequiredSkill; }
#endif
if (ic.CanBeSelected) selected = true;
if (ic.CanBeSelected) { selected = true; }
}
}
if (!picked) return false;
if (!picked) { return false; }
if (picker != null)
{
@@ -2345,7 +2385,7 @@ namespace Barotrauma
private void WritePropertyChange(IWriteMessage msg, object[] extraData, bool inGameEditableOnly)
{
var allProperties = inGameEditableOnly ? GetProperties<InGameEditable>() : GetProperties<Editable>();
var allProperties = inGameEditableOnly ? GetInGameEditableProperties() : GetProperties<Editable>();
SerializableProperty property = extraData[1] as SerializableProperty;
if (property != null)
{
@@ -2424,11 +2464,16 @@ namespace Barotrauma
}
}
private CoroutineHandle logPropertyChangeCoroutine;
private List<Pair<object, SerializableProperty>> GetInGameEditableProperties()
{
return GetProperties<ConditionallyEditable>()
.Where(ce => ce.Second.GetAttribute<ConditionallyEditable>().IsEditable())
.Union(GetProperties<InGameEditable>()).ToList();
}
private void ReadPropertyChange(IReadMessage msg, bool inGameEditableOnly, Client sender = null)
{
var allProperties = inGameEditableOnly ? GetProperties<InGameEditable>() : GetProperties<Editable>();
var allProperties = inGameEditableOnly ? GetInGameEditableProperties() : GetProperties<Editable>();
if (allProperties.Count == 0) { return; }
int propertyIndex = 0;
@@ -2579,18 +2624,31 @@ namespace Barotrauma
/// <returns></returns>
public static Item Load(XElement element, Submarine submarine, bool createNetworkEvent, IdRemap idRemap)
{
string name = element.Attribute("name").Value;
string name = element.Attribute("name").Value;
string identifier = element.GetAttributeString("identifier", "");
ItemPrefab prefab = ItemPrefab.Find(name, identifier);
if (prefab == null)
string pendingSwap = element.GetAttributeString("pendingswap", "");
ItemPrefab appliedSwap = null;
ItemPrefab oldPrefab = null;
if (!string.IsNullOrEmpty(pendingSwap) && Level.Loaded?.Type != LevelData.LevelType.Outpost)
{
return null;
oldPrefab = ItemPrefab.Find(name, identifier);
appliedSwap = ItemPrefab.Find(string.Empty, pendingSwap);
identifier = pendingSwap;
pendingSwap = null;
}
ItemPrefab prefab = ItemPrefab.Find(name, identifier);
if (prefab == null) { return null; }
Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
if (rect.Width == 0 && rect.Height == 0)
Vector2 centerPos = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2);
if (appliedSwap != null)
{
rect.Width = (int)(prefab.sprite.size.X * prefab.Scale);
rect.Height = (int)(prefab.sprite.size.Y * prefab.Scale);
}
else if (rect.Width == 0 && rect.Height == 0)
{
rect.Width = (int)(prefab.Size.X * prefab.Scale);
rect.Height = (int)(prefab.Size.Y * prefab.Scale);
@@ -2599,7 +2657,8 @@ namespace Barotrauma
Item item = new Item(rect, prefab, submarine, callOnItemLoaded: false, id: idRemap.GetOffsetId(element))
{
Submarine = submarine,
linkedToID = new List<ushort>()
linkedToID = new List<ushort>(),
PendingItemSwap = string.IsNullOrEmpty(pendingSwap) ? null : MapEntityPrefab.Find(pendingSwap) as ItemPrefab
};
#if SERVER
@@ -2609,7 +2668,7 @@ namespace Barotrauma
}
#endif
foreach (XAttribute attribute in element.Attributes())
foreach (XAttribute attribute in (appliedSwap?.ConfigElement ?? element).Attributes())
{
if (!item.SerializableProperties.TryGetValue(attribute.Name.ToString(), out SerializableProperty property)) { continue; }
bool shouldBeLoaded = false;
@@ -2622,14 +2681,14 @@ namespace Barotrauma
}
}
if (shouldBeLoaded)
if (shouldBeLoaded)
{
object prevValue = property.GetValue(item);
property.TrySetValue(item, attribute.Value);
//create network events for properties that differ from the prefab values
//(e.g. if a character has an item with modified colors in their inventory)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType<Editable>().Any() &&
(submarine == null || !submarine.Loading ))
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && property.Attributes.OfType<Editable>().Any() &&
(submarine == null || !submarine.Loading))
{
switch (property.Name)
{
@@ -2655,36 +2714,36 @@ namespace Barotrauma
//if we're overriding a non-overridden item in a sub/assembly xml or vice versa,
//use the values from the prefab instead of loading them from the sub/assembly xml
bool usePrefabValues = thisIsOverride != prefab.IsOverride;
bool usePrefabValues = thisIsOverride != prefab.IsOverride || appliedSwap != null;
List<ItemComponent> unloadedComponents = new List<ItemComponent>(item.components);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "upgrade":
{
var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty);
UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier);
int level = subElement.GetAttributeInt("level", 1);
if (upgradePrefab != null)
{
item.AddUpgrade(new Upgrade(item, upgradePrefab, level, subElement));
var upgradeIdentifier = subElement.GetAttributeString("identifier", string.Empty);
UpgradePrefab upgradePrefab = UpgradePrefab.Find(upgradeIdentifier);
int level = subElement.GetAttributeInt("level", 1);
if (upgradePrefab != null)
{
item.AddUpgrade(new Upgrade(item, upgradePrefab, level, subElement));
}
else
{
DebugConsole.AddWarning($"An upgrade with identifier \"{upgradeIdentifier}\" on {item.Name} was not found. " +
"It's effect will not be applied and won't be saved after the round ends.");
}
break;
}
else
default:
{
DebugConsole.AddWarning($"An upgrade with identifier \"{upgradeIdentifier}\" on {item.Name} was not found. " +
"It's effect will not be applied and won't be saved after the round ends.");
ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
if (component == null) { continue; }
component.Load(subElement, usePrefabValues, idRemap);
unloadedComponents.Remove(component);
break;
}
break;
}
default:
{
ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
if (component == null) { continue; }
component.Load(subElement, usePrefabValues, idRemap);
unloadedComponents.Remove(component);
break;
}
}
}
if (usePrefabValues)
@@ -2692,12 +2751,37 @@ namespace Barotrauma
//use prefab scale when overriding a non-overridden item or vice versa
item.Scale = prefab.ConfigElement.GetAttributeFloat(item.scale, "scale", "Scale");
}
item.Upgrades.ForEach(upgrade => upgrade.ApplyUpgrade());
var availableSwapIds = element.GetAttributeStringArray("availableswaps", new string[0]);
foreach (string swapId in availableSwapIds)
{
ItemPrefab swapPrefab = ItemPrefab.Find(string.Empty, swapId);
if (swapPrefab != null)
{
item.AvailableSwaps.Add(swapPrefab);
}
}
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
if (element.GetAttributeBool("flippedy", false)) { item.FlipY(false); }
if (appliedSwap != null && oldPrefab.SwappableItem != null && prefab.SwappableItem != null)
{
Vector2 oldRelativeOrigin = (oldPrefab.SwappableItem.SwapOrigin - oldPrefab.Size / 2) * element.GetAttributeFloat(item.scale, "scale", "Scale");
oldRelativeOrigin.Y = -oldRelativeOrigin.Y;
oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, item.rotationRad);
Vector2 oldOrigin = centerPos + oldRelativeOrigin;
Vector2 relativeOrigin = (prefab.SwappableItem.SwapOrigin - prefab.Size / 2) * item.Scale;
relativeOrigin.Y = -relativeOrigin.Y;
relativeOrigin = MathUtils.RotatePoint(relativeOrigin, item.rotationRad);
Vector2 origin = new Vector2(rect.X + rect.Width / 2, rect.Y - rect.Height / 2) + relativeOrigin;
item.rect.Location -= (origin - oldOrigin).ToPoint();
}
float condition = element.GetAttributeFloat("condition", item.MaxCondition);
item.condition = MathHelper.Clamp(condition, 0, item.MaxCondition);
item.lastSentCondition = item.condition;
@@ -2726,12 +2810,22 @@ namespace Barotrauma
new XAttribute("identifier", Prefab.Identifier),
new XAttribute("ID", ID));
if (PendingItemSwap != null)
{
element.Add(new XAttribute("pendingswap", PendingItemSwap.Identifier));
}
if (Rotation != 0f) { element.Add(new XAttribute("rotation", Rotation)); }
if (Prefab.IsOverride) { element.Add(new XAttribute("isoverride", "true")); }
if (FlippedX) { element.Add(new XAttribute("flippedx", true)); }
if (FlippedY) { element.Add(new XAttribute("flippedy", true)); }
if (AvailableSwaps.Any())
{
element.Add(new XAttribute("availableswaps", string.Join(',', AvailableSwaps.Select(s => s.Identifier))));
}
if (condition < MaxCondition)
{
element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture)));
@@ -192,6 +192,45 @@ namespace Barotrauma
}
}
class SwappableItem
{
public int BasePrice { get; }
public readonly bool CanBeBought;
public readonly string ReplacementOnUninstall;
public readonly Vector2 SwapOrigin;
public List<(string requiredTag, string swapTo)> ConnectedItemsToSwap = new List<(string requiredTag, string swapTo)>();
public int GetPrice(Location location = null)
{
int price = BasePrice;
return location?.GetAdjustedMechanicalCost(price) ?? price;
}
public SwappableItem(XElement element)
{
BasePrice = Math.Max(element.GetAttributeInt("price", 0), 0);
CanBeBought = element.GetAttributeBool("canbebought", BasePrice != 0);
ReplacementOnUninstall = element.GetAttributeString("replacementonuninstall", "");
SwapOrigin = element.GetAttributeVector2("origin", Vector2.One);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "swapconnecteditem":
ConnectedItemsToSwap.Add(
(subElement.GetAttributeString("tag", ""),
subElement.GetAttributeString("swapto", "")));
break;
}
}
}
}
partial class ItemPrefab : MapEntityPrefab, IHasUintIdentifier
{
private readonly string name;
@@ -461,6 +500,12 @@ namespace Barotrauma
private set;
} = new List<PreferredContainer>();
public SwappableItem SwappableItem
{
get;
private set;
}
/// <summary>
/// How likely it is for the item to spawn in a level of a given type.
/// Key = name of the LevelGenerationParameters (empty string = default value)
@@ -830,6 +875,17 @@ namespace Barotrauma
break;
}
#if CLIENT
case "upgradepreviewsprite":
{
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
}
UpgradePreviewSprite = new Sprite(subElement, iconFolder, lazyLoad: true);
UpgradePreviewScale = subElement.GetAttributeFloat("scale", 1.0f);
}
break;
case "inventoryicon":
{
string iconFolder = "";
@@ -862,15 +918,15 @@ namespace Barotrauma
}
break;
case "damagedinfectedsprite":
{
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
}
string iconFolder = "";
if (!subElement.GetAttributeString("texture", "").Contains("/"))
{
iconFolder = Path.GetDirectoryName(filePath);
}
DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true);
}
DamagedInfectedSprite = new Sprite(subElement, iconFolder, lazyLoad: true);
}
break;
case "brokensprite":
string brokenSpriteFolder = "";
@@ -963,6 +1019,9 @@ namespace Barotrauma
PreferredContainers.Add(preferredContainer);
}
break;
case "swappableitem":
SwappableItem = new SwappableItem(subElement);
break;
case "trigger":
Rectangle trigger = new Rectangle(0, 0, 10, 10)
{
@@ -1144,6 +1203,54 @@ namespace Barotrauma
}
}
public List<Tuple<string, PriceInfo>> GetBuyPricesUnder(int maxCost = 0)
{
List<Tuple<string, PriceInfo>> priceLocations = new List<Tuple<string, PriceInfo>>();
foreach (KeyValuePair<string, PriceInfo> locationPrice in locationPrices)
{
PriceInfo priceInfo = locationPrice.Value;
if (priceInfo == null)
{
continue;
}
if (!priceInfo.CanBeBought)
{
continue;
}
if (priceInfo.Price < maxCost || maxCost == 0)
{
priceLocations.Add(new Tuple<string, PriceInfo>(locationPrice.Key, priceInfo));
}
}
return priceLocations;
}
public List<Tuple<string, PriceInfo>> GetSellPricesOver(int minCost = 0, bool sellingImportant = true)
{
List<Tuple<string, PriceInfo>> priceLocations = new List<Tuple<string, PriceInfo>>();
if (!CanBeSold && sellingImportant)
{
return priceLocations;
}
foreach (KeyValuePair<string, PriceInfo> locationPrice in locationPrices)
{
PriceInfo priceInfo = locationPrice.Value;
if (priceInfo == null)
{
continue;
}
if (priceInfo.Price > minCost)
{
priceLocations.Add(new Tuple<string, PriceInfo>(locationPrice.Key, priceInfo));
}
}
return priceLocations;
}
public bool IsContainerPreferred(Item item, ItemContainer targetContainer, out bool isPreferencesDefined, out bool isSecondary)
{
isPreferencesDefined = PreferredContainers.Any();