Unstable 1.2.4.0
This commit is contained in:
@@ -91,6 +91,11 @@ namespace Barotrauma
|
||||
private IEnumerable<Hull> visibleHulls;
|
||||
private float hullVisibilityTimer;
|
||||
const float hullVisibilityInterval = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Returns hulls that are visible to the character, including the current hull.
|
||||
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
|
||||
/// </summary>
|
||||
public IEnumerable<Hull> VisibleHulls
|
||||
{
|
||||
get
|
||||
@@ -353,7 +358,7 @@ namespace Barotrauma
|
||||
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
|
||||
{
|
||||
var inventory = parentItem.OwnInventory;
|
||||
if (inventory == null) { return; }
|
||||
if (inventory == null || !inventory.Container.DrawInventory) { return; }
|
||||
int removed = 0;
|
||||
if (predicate == null || inventory.AllItems.Any(predicate))
|
||||
{
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace Barotrauma
|
||||
|
||||
if (aiElements.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.",
|
||||
DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.",
|
||||
contentPackage: c.Prefab?.ContentPackage);
|
||||
outsideSteering = new SteeringManager(this);
|
||||
insideSteering = new IndoorsSteeringManager(this, false, false);
|
||||
@@ -312,7 +312,7 @@ namespace Barotrauma
|
||||
}
|
||||
ReevaluateAttacks();
|
||||
outsideSteering = new SteeringManager(this);
|
||||
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
|
||||
insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors);
|
||||
steeringManager = outsideSteering;
|
||||
State = AIState.Idle;
|
||||
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
|
||||
@@ -322,6 +322,10 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private CharacterParams.AIParams _aiParams;
|
||||
/// <summary>
|
||||
/// Shorthand for <see cref="Character.Params.AI"/> with null checking.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Character.Params.AI"/> or an empty params. Does not return nulls.</returns>
|
||||
public CharacterParams.AIParams AIParams
|
||||
{
|
||||
get
|
||||
@@ -565,7 +569,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
|
||||
if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors)
|
||||
{
|
||||
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
|
||||
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
|
||||
@@ -3097,7 +3101,10 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
|
||||
valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist);
|
||||
valueModifier *=
|
||||
targetMemory.Priority /
|
||||
//sqrt = the further the target is, the less the distance matters
|
||||
MathF.Sqrt(dist);
|
||||
|
||||
if (valueModifier > targetValue)
|
||||
{
|
||||
|
||||
@@ -685,8 +685,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (removeDivingSuit)
|
||||
{
|
||||
var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear);
|
||||
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors))
|
||||
var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear);
|
||||
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character))
|
||||
{
|
||||
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
@@ -727,54 +727,51 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (takeMaskOff)
|
||||
{
|
||||
if (Character.HasEquippedItem(Tags.LightDivingGear))
|
||||
{
|
||||
var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear);
|
||||
if (mask != null)
|
||||
{
|
||||
var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear);
|
||||
if (mask != null)
|
||||
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
{
|
||||
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
|
||||
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
{
|
||||
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
|
||||
mask.Drop(Character);
|
||||
HandleRelocation(mask);
|
||||
ReequipUnequipped();
|
||||
}
|
||||
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
|
||||
{
|
||||
findItemState = FindItemState.DivingMask;
|
||||
if (FindSuitableContainer(mask, out Item targetContainer))
|
||||
{
|
||||
mask.Drop(Character);
|
||||
HandleRelocation(mask);
|
||||
ReequipUnequipped();
|
||||
}
|
||||
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
|
||||
{
|
||||
findItemState = FindItemState.DivingMask;
|
||||
if (FindSuitableContainer(mask, out Item targetContainer))
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
findItemState = FindItemState.None;
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
decontainObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
mask.Drop(Character);
|
||||
HandleRelocation(mask);
|
||||
ReequipUnequipped();
|
||||
}
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
decontainObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
mask.Drop(Character);
|
||||
HandleRelocation(mask);
|
||||
ReequipUnequipped();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReequipUnequipped();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReequipUnequipped();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,13 +781,11 @@ namespace Barotrauma
|
||||
|
||||
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
foreach (Item item in Character.HeldItems)
|
||||
{
|
||||
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand;
|
||||
Item item = Character.Inventory.GetItemInLimbSlot(hand);
|
||||
if (item == null) { continue; }
|
||||
if (item == null || !item.IsInteractable(Character)) { continue; }
|
||||
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
|
||||
{
|
||||
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
|
||||
findItemState = FindItemState.OtherItem;
|
||||
@@ -1389,7 +1384,10 @@ namespace Barotrauma
|
||||
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
|
||||
continue;
|
||||
}
|
||||
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
|
||||
bool isWitnessing =
|
||||
otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) ||
|
||||
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
|
||||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
|
||||
if (!isWitnessing)
|
||||
{
|
||||
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
|
||||
@@ -1754,11 +1752,11 @@ namespace Barotrauma
|
||||
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
|
||||
otherCharacter.Info?.Job == null ||
|
||||
otherCharacter.AIController is not HumanAIController otherHumanAI ||
|
||||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
|
||||
Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!otherCharacter.CanSeeTarget(character)) { continue; }
|
||||
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
|
||||
|
||||
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
|
||||
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
|
||||
@@ -1840,13 +1838,13 @@ namespace Barotrauma
|
||||
foreach (Character otherCharacter in Character.CharacterList)
|
||||
{
|
||||
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
|
||||
otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) ||
|
||||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
|
||||
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
|
||||
Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
|
||||
if (!otherCharacter.CanSeeTarget(thief)) { continue; }
|
||||
if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; }
|
||||
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
|
||||
// -> allow them to use the emergency items
|
||||
if (thief.Submarine != null)
|
||||
|
||||
+5
-1
@@ -1070,7 +1070,8 @@ namespace Barotrauma
|
||||
{
|
||||
// Try reload ammunition from inventory
|
||||
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
|
||||
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
|
||||
Item ammunition = character.Inventory.FindItem(i =>
|
||||
i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
|
||||
if (ammunition != null)
|
||||
{
|
||||
var container = Weapon.GetComponent<ItemContainer>();
|
||||
@@ -1089,6 +1090,9 @@ namespace Barotrauma
|
||||
}
|
||||
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
{
|
||||
// Inventory not drawn = it's not interactable
|
||||
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
|
||||
if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
|
||||
SeekAmmunition(ammunitionIdentifiers);
|
||||
}
|
||||
return false;
|
||||
|
||||
+6
-4
@@ -14,7 +14,7 @@ namespace Barotrauma
|
||||
private int escapeProgress;
|
||||
private bool isBeingWatched;
|
||||
|
||||
private bool shouldSwitchTeams;
|
||||
private readonly bool shouldSwitchTeams;
|
||||
|
||||
const string EscapeTeamChangeIdentifier = "escape";
|
||||
|
||||
@@ -88,10 +88,12 @@ namespace Barotrauma
|
||||
escapeProgress += Rand.Range(2, 5);
|
||||
if (escapeProgress > 15)
|
||||
{
|
||||
Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem);
|
||||
if (handcuffs != null)
|
||||
foreach (var it in character.HeldItems)
|
||||
{
|
||||
handcuffs.Drop(character);
|
||||
if (it.HasTag(Tags.HandLockerItem) && it.IsInteractable(character))
|
||||
{
|
||||
it.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ namespace Barotrauma
|
||||
if (!IsValidTarget(target, character)) { continue; }
|
||||
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
|
||||
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
|
||||
character.CanSeeTarget(target))
|
||||
character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
{
|
||||
AIObjectiveCheckStolenItems? existingObjective =
|
||||
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ namespace Barotrauma
|
||||
// The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
|
||||
timerMargin += 0.5f;
|
||||
timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
|
||||
newTargetTimer = Math.Min(newTargetTimer, timerMargin);
|
||||
newTargetTimer = Math.Max(newTargetTimer, timerMargin);
|
||||
}
|
||||
|
||||
private void SetTargetTimerHigh()
|
||||
|
||||
@@ -664,6 +664,13 @@ namespace Barotrauma
|
||||
WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex;
|
||||
|
||||
UseController = useController ?? other.UseController;
|
||||
|
||||
#if DEBUG
|
||||
if (UseController && ConnectedController == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public Order WithOption(Identifier option)
|
||||
@@ -713,7 +720,12 @@ namespace Barotrauma
|
||||
|
||||
public Order WithItemComponent(Item item, ItemComponent component = null)
|
||||
{
|
||||
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item));
|
||||
Controller controller = null;
|
||||
if (UseController)
|
||||
{
|
||||
controller = item?.FindController(tags: ControllerTags);
|
||||
}
|
||||
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller);
|
||||
}
|
||||
|
||||
public Order WithWallSection(Structure wall, int? sectionIndex)
|
||||
|
||||
+45
-5
@@ -10,6 +10,17 @@ namespace Barotrauma
|
||||
{
|
||||
class HumanoidAnimController : AnimController
|
||||
{
|
||||
private const float SteepestWalkableSlopeAngleDegrees = 50f;
|
||||
private const float SlowlyWalkableSlopeAngleDegrees = 30f;
|
||||
|
||||
private static readonly float SteepestWalkableSlopeNormalX =
|
||||
MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees));
|
||||
private static readonly float SlowlyWalkableSlopeNormalX =
|
||||
MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees));
|
||||
|
||||
private const float MaxSpeedOnStairs = 1.7f;
|
||||
private const float SteepSlopePushMagnitude = MaxSpeedOnStairs;
|
||||
|
||||
public override RagdollParams RagdollParams
|
||||
{
|
||||
get { return HumanRagdollParams; }
|
||||
@@ -501,10 +512,14 @@ namespace Barotrauma
|
||||
Limb leftLeg = GetLimb(LimbType.LeftLeg);
|
||||
Limb rightLeg = GetLimb(LimbType.RightLeg);
|
||||
|
||||
bool onSlopeThatMakesSlow = Math.Abs(floorNormal.X) > SlowlyWalkableSlopeNormalX;
|
||||
bool slowedDownBySlope = onSlopeThatMakesSlow && Math.Sign(floorNormal.X) == -Math.Sign(TargetMovement.X);
|
||||
bool onSlopeTooSteepToClimb = Math.Abs(floorNormal.X) > SteepestWalkableSlopeNormalX;
|
||||
|
||||
float walkCycleMultiplier = 1.0f;
|
||||
if (Stairs != null)
|
||||
if (Stairs != null || slowedDownBySlope)
|
||||
{
|
||||
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -1.7f, 1.7f), TargetMovement.Y);
|
||||
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -MaxSpeedOnStairs, MaxSpeedOnStairs), TargetMovement.Y);
|
||||
walkCycleMultiplier *= 1.5f;
|
||||
}
|
||||
|
||||
@@ -586,6 +601,15 @@ namespace Barotrauma
|
||||
|
||||
bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f);
|
||||
|
||||
if (Stairs == null && onSlopeTooSteepToClimb)
|
||||
{
|
||||
if (Math.Sign(targetMovement.X) != Math.Sign(floorNormal.X))
|
||||
{
|
||||
targetMovement.X = Math.Sign(floorNormal.X) * SteepSlopePushMagnitude;
|
||||
movement = targetMovement;
|
||||
}
|
||||
}
|
||||
|
||||
if (Stairs != null || onSlope)
|
||||
{
|
||||
torso.PullJointWorldAnchorB = new Vector2(
|
||||
@@ -764,7 +788,7 @@ namespace Barotrauma
|
||||
{
|
||||
footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f);
|
||||
}
|
||||
if (Stairs == null)
|
||||
if (Stairs == null && !onSlopeThatMakesSlow)
|
||||
{
|
||||
footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y);
|
||||
}
|
||||
@@ -1536,7 +1560,7 @@ namespace Barotrauma
|
||||
target.CharacterHealth.CalculateVitality();
|
||||
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
|
||||
{
|
||||
character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive);
|
||||
character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive);
|
||||
SteamAchievementManager.OnCharacterRevived(target, character);
|
||||
lastReviveTime = (float)Timing.TotalTime;
|
||||
#if SERVER
|
||||
@@ -1741,7 +1765,23 @@ namespace Barotrauma
|
||||
targetAnchor += target.Submarine.SimPosition;
|
||||
}
|
||||
}
|
||||
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
|
||||
if (Vector2.DistanceSquared(pullLimb.PullJointWorldAnchorA, pullLimbAnchor) > 50.0f * 50.0f)
|
||||
{
|
||||
//there's a similar error check in the PullJointWorldAnchorB setter, but we seem to be getting quite a lot of
|
||||
//errors specifically from this method, so let's use a more consistent error message here to prevent clogging GA with
|
||||
//different error messages that all include a different coordinate
|
||||
string errorMsg =
|
||||
$"Attempted to move the anchor B of a limb's pull joint extremely far from the limb in {nameof(DragCharacter)}. " +
|
||||
$"Character in sub: {character.Submarine != null}, target in sub: {target.Submarine != null}.";
|
||||
GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -572,6 +572,10 @@ namespace Barotrauma
|
||||
|
||||
public void AddJoint(JointParams jointParams)
|
||||
{
|
||||
if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
|
||||
GameMain.World.Add(joint.Joint);
|
||||
for (int i = 0; i < LimbJoints.Length; i++)
|
||||
@@ -582,6 +586,21 @@ namespace Barotrauma
|
||||
}
|
||||
Array.Resize(ref LimbJoints, LimbJoints.Length + 1);
|
||||
LimbJoints[LimbJoints.Length - 1] = joint;
|
||||
|
||||
bool checkLimbIndex(int index, string debugName)
|
||||
{
|
||||
if (index < 0 || index >= limbs.Length)
|
||||
{
|
||||
string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}.";
|
||||
DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage);
|
||||
if (jointParams.Element?.ContentPackage == GameMain.VanillaContent)
|
||||
{
|
||||
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected void AddLimb(LimbParams limbParams)
|
||||
@@ -668,6 +687,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private enum LimbStairCollisionResponse
|
||||
{
|
||||
DontClimbStairs,
|
||||
ClimbWithoutLimbCollision,
|
||||
ClimbWithLimbCollision
|
||||
}
|
||||
|
||||
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
|
||||
{
|
||||
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
|
||||
@@ -710,37 +736,53 @@ namespace Barotrauma
|
||||
}
|
||||
else if (structure.StairDirection != Direction.None)
|
||||
{
|
||||
Stairs = null;
|
||||
|
||||
//don't collider with stairs if
|
||||
|
||||
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
|
||||
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
|
||||
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return false; }
|
||||
|
||||
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
|
||||
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return false; }
|
||||
|
||||
//3. collided with the stairs from below
|
||||
if (contact.Manifold.LocalNormal.Y < 0.0f) { return false; }
|
||||
|
||||
//4. contact points is above the bottom half of the collider
|
||||
contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
|
||||
if (points[0].Y > Collider.SimPosition.Y) { return false; }
|
||||
|
||||
//5. in water
|
||||
if (inWater && targetMovement.Y < 0.5f) { return false; }
|
||||
|
||||
//---------------
|
||||
|
||||
//set stairs to that of the one dragging us
|
||||
if (character.SelectedBy != null)
|
||||
{
|
||||
Stairs = character.SelectedBy.AnimController.Stairs;
|
||||
}
|
||||
else
|
||||
Stairs = structure;
|
||||
{
|
||||
var collisionResponse = handleLimbStairCollision();
|
||||
if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision)
|
||||
{
|
||||
Stairs = structure;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; }
|
||||
|
||||
if (Stairs == null)
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LimbStairCollisionResponse handleLimbStairCollision()
|
||||
{
|
||||
//don't collide with stairs if
|
||||
|
||||
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
|
||||
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
|
||||
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
|
||||
|
||||
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
|
||||
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; }
|
||||
|
||||
//3. collided with the stairs from below
|
||||
if (contact.Manifold.LocalNormal.Y < 0.0f)
|
||||
{
|
||||
return Stairs != structure
|
||||
? LimbStairCollisionResponse.DontClimbStairs
|
||||
: LimbStairCollisionResponse.ClimbWithoutLimbCollision;
|
||||
}
|
||||
|
||||
//4. contact points is above the bottom half of the collider
|
||||
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
|
||||
if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; }
|
||||
|
||||
//5. in water
|
||||
if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
|
||||
|
||||
return LimbStairCollisionResponse.ClimbWithLimbCollision;
|
||||
}
|
||||
}
|
||||
|
||||
lock (impactQueue)
|
||||
@@ -1335,17 +1377,28 @@ namespace Barotrauma
|
||||
if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
|
||||
{
|
||||
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
|
||||
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f)
|
||||
|
||||
const float LevitationSpeedMultiplier = 5f;
|
||||
|
||||
// If the character is walking down a slope, target a position that moves along it
|
||||
float slopePull = 0f;
|
||||
if (floorNormal.Y is > 0f and < 1f
|
||||
&& Math.Sign(movement.X) == Math.Sign(floorNormal.X))
|
||||
{
|
||||
if (Stairs != null)
|
||||
slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier;
|
||||
}
|
||||
|
||||
if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f)
|
||||
{
|
||||
float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier;
|
||||
if (Stairs != null && targetY < Collider.SimPosition.Y)
|
||||
{
|
||||
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
|
||||
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
|
||||
yVelocity = Math.Sign(yVelocity);
|
||||
}
|
||||
|
||||
yVelocity -= slopePull * LevitationSpeedMultiplier;
|
||||
|
||||
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -1593,10 +1646,10 @@ namespace Barotrauma
|
||||
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
|
||||
private const float FloorYStaleTime = 1;
|
||||
private float floorYCheckTimer;
|
||||
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
|
||||
private void RefreshFloorY(float deltaTime, bool ignoreStairs = false)
|
||||
{
|
||||
floorYCheckTimer -= deltaTime;
|
||||
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
|
||||
PhysicsBody refBody = Collider;
|
||||
if (floorYCheckTimer < 0 ||
|
||||
lastFloorCheckIgnoreStairs != ignoreStairs ||
|
||||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
|
||||
@@ -1621,7 +1674,7 @@ namespace Barotrauma
|
||||
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
|
||||
if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); }
|
||||
|
||||
Vector2 rayEnd = rayStart - new Vector2(0.0f, height);
|
||||
Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f);
|
||||
Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
|
||||
|
||||
Fixture standOnFloorFixture = null;
|
||||
@@ -1700,7 +1753,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (closestFraction == 1) //raycast didn't hit anything
|
||||
if (closestFraction >= 1) //raycast didn't hit anything
|
||||
{
|
||||
floorNormal = Vector2.UnitY;
|
||||
if (CurrentHull == null)
|
||||
|
||||
@@ -67,10 +67,18 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (var item in HeldItems)
|
||||
{
|
||||
if (item.body != null)
|
||||
if (item.body == null) { continue; }
|
||||
if (!enabled)
|
||||
{
|
||||
item.body.Enabled = enabled;
|
||||
item.body.Enabled = false;
|
||||
}
|
||||
else if (item.GetComponent<Holdable>() is { IsActive: true })
|
||||
{
|
||||
//held items includes all items in hand slots
|
||||
//we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs
|
||||
item.body.Enabled = true;
|
||||
}
|
||||
|
||||
}
|
||||
AnimController.Collider.Enabled = value;
|
||||
}
|
||||
@@ -936,10 +944,16 @@ namespace Barotrauma
|
||||
{
|
||||
var prevSelectedItem = _selectedItem;
|
||||
_selectedItem = value;
|
||||
if (value is not null)
|
||||
{
|
||||
CheckTalents(AbilityEffectType.OnItemSelected, new AbilityItemSelected(value));
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem);
|
||||
if (Controlled == this)
|
||||
{
|
||||
_selectedItem?.GetComponent<Fabricator>()?.RefreshSelectedItem();
|
||||
|
||||
if (_selectedItem == null)
|
||||
{
|
||||
GameMain.GameSession?.CrewManager?.ResetCrewList();
|
||||
@@ -1694,31 +1708,21 @@ namespace Barotrauma
|
||||
info.Job?.GiveJobItems(this, spawnPoint);
|
||||
}
|
||||
|
||||
|
||||
public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
|
||||
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
|
||||
{
|
||||
GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent);
|
||||
}
|
||||
|
||||
public void GiveIdCardTags(IEnumerable<WayPoint> spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
|
||||
{
|
||||
if (info?.Job == null || spawnPoints == null) { return; }
|
||||
if (info?.Job == null || spawnPoint == null) { return; }
|
||||
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
{
|
||||
if (item?.GetComponent<IdCard>() is not IdCard idCard) { continue; }
|
||||
if (requireSpawnPointTagsNotGiven)
|
||||
var idCard = item?.GetComponent<IdCard>();
|
||||
if (idCard == null) { continue; }
|
||||
//if the card belongs to someone else, don't add any tags.
|
||||
//otherwise you can gain access to places you shouldn't by temporarily giving the card to someone (e.g. a captain bot) at the end of the round
|
||||
if (idCard.OwnerName != info.Name) { continue; }
|
||||
foreach (string s in spawnPoint.IdCardTags)
|
||||
{
|
||||
if (idCard.SpawnPointTagsGiven) { continue; }
|
||||
item.AddTag(s);
|
||||
}
|
||||
foreach (var spawnPoint in spawnPoints)
|
||||
{
|
||||
foreach (string s in spawnPoint.IdCardTags)
|
||||
{
|
||||
item.AddTag(s);
|
||||
}
|
||||
}
|
||||
idCard.SpawnPointTagsGiven = true;
|
||||
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
|
||||
@@ -2303,40 +2307,40 @@ namespace Barotrauma
|
||||
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
|
||||
}
|
||||
|
||||
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false)
|
||||
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
|
||||
if (target is Character targetCharacter)
|
||||
{
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CheckVisibility(target, seeingEntity, checkFacing);
|
||||
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
|
||||
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
if (seeingEntity is Character seeingCharacter)
|
||||
{
|
||||
return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing);
|
||||
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
|
||||
}
|
||||
if (target is Character targetCharacter)
|
||||
{
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
|
||||
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CheckVisibility(target, seeingEntity, checkFacing);
|
||||
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false)
|
||||
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; }
|
||||
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (!target.AnimController.SimplePhysicsEnabled)
|
||||
{
|
||||
//find the limbs that are furthest from the target's position (from the viewer's point of view)
|
||||
@@ -2365,13 +2369,13 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; }
|
||||
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; }
|
||||
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
|
||||
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(target != null);
|
||||
if (target == null) { return false; }
|
||||
@@ -2382,39 +2386,37 @@ namespace Barotrauma
|
||||
{
|
||||
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
|
||||
}
|
||||
Body closestBody;
|
||||
//both inside the same sub (or both outside)
|
||||
//OR the we're inside, the other character outside
|
||||
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
|
||||
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//we're outside, the other character inside
|
||||
else if (seeingEntity.Submarine == null)
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
|
||||
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
//both inside different subs
|
||||
else
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
|
||||
if (!IsBlocking(closestBody))
|
||||
{
|
||||
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
|
||||
}
|
||||
return
|
||||
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
|
||||
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
|
||||
}
|
||||
return !IsBlocking(closestBody);
|
||||
|
||||
bool IsBlocking(Body body)
|
||||
bool IsBlocking(Fixture f)
|
||||
{
|
||||
var body = f.Body;
|
||||
if (body == null) { return false; }
|
||||
if (body.UserData is Structure wall && wall.CastShadow)
|
||||
if (body.UserData is Structure wall)
|
||||
{
|
||||
if (!wall.CastShadow && seeThroughWindows) { return false; }
|
||||
return wall != target;
|
||||
}
|
||||
else if (body.UserData is Item item)
|
||||
{
|
||||
if (item.GetComponent<Door>() is { HasWindow: true } door)
|
||||
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
|
||||
{
|
||||
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
|
||||
}
|
||||
@@ -2513,9 +2515,21 @@ namespace Barotrauma
|
||||
|
||||
if (inventory.Owner is Item item)
|
||||
{
|
||||
if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
|
||||
ItemContainer container = item.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
|
||||
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
|
||||
if (!CanInteractWith(item))
|
||||
{
|
||||
//could be simplified with LINQ, but that'd require capturing variables which we shouldn't do in a method that's called as frequently as this
|
||||
foreach (var linkedEntity in item.linkedTo)
|
||||
{
|
||||
if (linkedEntity is Item linkedItem && linkedItem.DisplaySideBySideWhenLinked && CanInteractWith(linkedItem)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
ItemContainer container = (inventory as ItemInventory)?.Container;
|
||||
if (container != null)
|
||||
{
|
||||
if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
|
||||
if (!container.DrawInventory) { return false; }
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -3584,6 +3598,8 @@ namespace Barotrauma
|
||||
|
||||
private void Despawn(bool createNetworkEvents = true)
|
||||
{
|
||||
if (!EnableDespawn) { return; }
|
||||
|
||||
Identifier despawnContainerId =
|
||||
IsHuman ?
|
||||
"despawncontainer".ToIdentifier() :
|
||||
@@ -3665,10 +3681,12 @@ namespace Barotrauma
|
||||
float massFactor = (float)Math.Sqrt(Mass / 20);
|
||||
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
|
||||
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
|
||||
newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier);
|
||||
if (!float.IsNaN(newRange))
|
||||
{
|
||||
aiTarget.SightRange = newRange;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void UpdateSoundRange(float deltaTime)
|
||||
@@ -3683,6 +3701,7 @@ namespace Barotrauma
|
||||
float massFactor = (float)Math.Sqrt(Mass / 10);
|
||||
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
|
||||
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
|
||||
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
|
||||
if (!float.IsNaN(newRange))
|
||||
{
|
||||
aiTarget.SoundRange = newRange;
|
||||
@@ -4340,19 +4359,16 @@ namespace Barotrauma
|
||||
}
|
||||
if (medicalDamage > 0)
|
||||
{
|
||||
IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
|
||||
IncreaseSkillLevel(Tags.MedicalSkill, medicalDamage);
|
||||
}
|
||||
if (weaponDamage > 0)
|
||||
{
|
||||
IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage);
|
||||
IncreaseSkillLevel(Tags.WeaponsSkill, weaponDamage);
|
||||
}
|
||||
|
||||
void IncreaseSkillLevel(Identifier skill, float damage)
|
||||
{
|
||||
float attackerSkillLevel = attacker.GetSkillLevel(skill);
|
||||
// The formula is too generous on low skill levels, hence the minimum divider.
|
||||
float minSkillDivider = 15f;
|
||||
attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider));
|
||||
attacker.Info?.ApplySkillGain(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage, false, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4366,12 +4382,10 @@ namespace Barotrauma
|
||||
{
|
||||
medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
|
||||
}
|
||||
if (medicalGain <= 0) { return; }
|
||||
Identifier skill = new Identifier("medical");
|
||||
float attackerSkillLevel = healer.GetSkillLevel(skill);
|
||||
// The formula is too generous on low skill levels, hence the minimum divider.
|
||||
float minSkillDivider = 15f;
|
||||
healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider));
|
||||
if (medicalGain > 0)
|
||||
{
|
||||
healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -5005,8 +5019,10 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<Hull> visibleHulls = new List<Hull>();
|
||||
private readonly HashSet<Hull> tempList = new HashSet<Hull>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns hulls that are visible to the player, including the current hull.
|
||||
/// Returns hulls that are visible to the character, including the current hull.
|
||||
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
|
||||
/// Can be heavy if used every frame.
|
||||
/// </summary>
|
||||
public List<Hull> GetVisibleHulls()
|
||||
@@ -5021,7 +5037,7 @@ namespace Barotrauma
|
||||
foreach (var hull in adjacentHulls)
|
||||
{
|
||||
if (hull.ConnectedGaps.Any(g =>
|
||||
(g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) &&
|
||||
g.Open > 0.9f &&
|
||||
g.linkedTo.Contains(CurrentHull) &&
|
||||
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2)))
|
||||
{
|
||||
@@ -5041,7 +5057,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
if (h.ConnectedGaps.Any(g =>
|
||||
(g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) &&
|
||||
g.Open > 0.9f &&
|
||||
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) &&
|
||||
CanSeeTarget(g)))
|
||||
{
|
||||
@@ -5582,4 +5598,12 @@ namespace Barotrauma
|
||||
public Character Character { get; set; }
|
||||
}
|
||||
|
||||
class AbilityItemSelected : AbilityObject, IAbilityItem
|
||||
{
|
||||
public AbilityItemSelected(Item item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
public Item Item { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1214,6 +1214,21 @@ namespace Barotrauma
|
||||
return (int)(salary * Job.Prefab.PriceMultiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increases the characters skill at a rate proportional to their current skill.
|
||||
/// If you want to increase the skill level by a specific amount instead, use <see cref="IncreaseSkillLevel"/>
|
||||
/// </summary>
|
||||
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f)
|
||||
{
|
||||
float skillLevel = Job.GetSkillLevel(skillIdentifier);
|
||||
// The formula is too generous on low skill levels, hence the minimum divider.
|
||||
float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent);
|
||||
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
|
||||
/// </summary>
|
||||
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false)
|
||||
{
|
||||
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
|
||||
@@ -1222,9 +1237,7 @@ namespace Barotrauma
|
||||
{
|
||||
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
|
||||
}
|
||||
|
||||
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
|
||||
|
||||
increase = GetSkillSpecificGain(increase, skillIdentifier);
|
||||
|
||||
float prevLevel = Job.GetSkillLevel(skillIdentifier);
|
||||
@@ -1902,13 +1915,30 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the combined stat value of the identifier "all" and the specified identifier.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The "all" identifier works like the "any" identifier in outpost modules where it doesn't literally mean everything but
|
||||
/// is an unique identifier that indicates that it should target everything. For example if we wanted to make a talent
|
||||
/// that increases the fabrication quality of every single item we could use something like:
|
||||
/// <CharacterAbilityGivePermanentStat stattype="IncreaseFabricationQuality" statidentifier="all" />
|
||||
/// (Granted IncreaseFabricationQuality doesn't support the "all" identifier so if we need this in vanilla it needs to be implemented in code)
|
||||
/// </remarks>
|
||||
public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier)
|
||||
=> GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) +
|
||||
GetSavedStatValue(statType, statIdentifier);
|
||||
|
||||
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
|
||||
=> GetSavedStatValueWithBotsInMp(statType, statIdentifier, GameSession.GetSessionCrewCharacters(CharacterType.Bot));
|
||||
|
||||
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection<Character> bots)
|
||||
{
|
||||
float statValue = GetSavedStatValue(statType, statIdentifier);
|
||||
|
||||
if (GameMain.NetworkMember is null) { return statValue; }
|
||||
|
||||
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
|
||||
foreach (Character bot in bots)
|
||||
{
|
||||
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
|
||||
statValue = Math.Max(statValue, botStatValue);
|
||||
|
||||
+4
@@ -1026,6 +1026,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all the effects of the prefab (including the sounds and other assets defined in them).
|
||||
/// Note that you need to call LoadAllEffectsAndTreatmentSuitabilities before trying to use the affliction again!
|
||||
/// </summary>
|
||||
public static void ClearAllEffects()
|
||||
{
|
||||
Prefabs.ForEach(p => p.ClearEffects());
|
||||
|
||||
@@ -1313,6 +1313,8 @@ namespace Barotrauma
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToUpdate.Clear();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
|
||||
@@ -136,6 +136,12 @@ namespace Barotrauma
|
||||
public readonly List<ParticleParams> DamageEmitters = new List<ParticleParams>();
|
||||
public readonly List<InventoryParams> Inventories = new List<InventoryParams>();
|
||||
public HealthParams Health { get; private set; }
|
||||
/// <summary>
|
||||
/// Parameters for EnemyAIController. Not used by HumanAIController.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// AIParams or null. Use <see cref="EnemyAIController.AIParams"/>, if you don't expect nulls.
|
||||
/// </returns>
|
||||
public AIParams AI { get; private set; }
|
||||
|
||||
public CharacterParams(CharacterFile file)
|
||||
@@ -603,7 +609,8 @@ namespace Barotrauma
|
||||
|
||||
public void AddItem(string identifier = null)
|
||||
{
|
||||
identifier = identifier ?? "";
|
||||
if (Element == null) { return; }
|
||||
identifier ??= "";
|
||||
var element = CreateElement("item", new XAttribute("identifier", identifier));
|
||||
Element.Add(element);
|
||||
var item = new InventoryItem(element, Character);
|
||||
@@ -732,6 +739,11 @@ namespace Barotrauma
|
||||
|
||||
public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams)
|
||||
{
|
||||
if (Element == null)
|
||||
{
|
||||
targetParams = null;
|
||||
return false;
|
||||
}
|
||||
var element = TargetParams.CreateNewElement(Character, tag, state, priority);
|
||||
if (TryAddTarget(element, out targetParams))
|
||||
{
|
||||
|
||||
@@ -100,6 +100,13 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(1.5f, IsPropertySaveable.Yes)]
|
||||
public float SkillIncreaseExponent
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier())
|
||||
{
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
+38
-16
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -11,6 +10,12 @@ namespace Barotrauma.Abilities
|
||||
|
||||
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, the conditional is checked on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
|
||||
/// Defaults to true, except in the case of <see cref="AbilityConditionHasPermanentStat"/>, which by default targets the character who has the talent.
|
||||
/// </summary>
|
||||
private readonly bool targetAbilityTarget = false;
|
||||
|
||||
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
|
||||
{
|
||||
targetTypes = ParseTargetTypes(
|
||||
@@ -25,30 +30,47 @@ namespace Barotrauma.Abilities
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetTypes.Any() && !conditionals.Any())
|
||||
//don't log this error if this is a subclass of AbilityConditionCharacter
|
||||
//(in that case not having any conditionals here is ok)
|
||||
if (!targetTypes.Any() && !conditionals.Any() && GetType() == typeof(AbilityConditionCharacter))
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.",
|
||||
contentPackage: conditionElement.ContentPackage);
|
||||
}
|
||||
|
||||
targetAbilityTarget = conditionElement.GetAttributeBool(nameof(targetAbilityTarget), this is not AbilityConditionHasPermanentStat);
|
||||
}
|
||||
|
||||
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
public sealed override bool MatchesCondition()
|
||||
{
|
||||
if (abilityObject is IAbilityCharacter abilityCharacter)
|
||||
//by default data-reliant conditions don't accept null, but in this case it's ok,
|
||||
//because we can assume it's the character who has the talent
|
||||
return MatchesCondition(abilityObject: null);
|
||||
}
|
||||
|
||||
public sealed override bool MatchesCondition(AbilityObject abilityObject)
|
||||
{
|
||||
return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject);
|
||||
}
|
||||
|
||||
protected sealed override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
{
|
||||
Character targetCharacter =
|
||||
targetAbilityTarget ?
|
||||
(abilityObject as IAbilityCharacter)?.Character ?? character :
|
||||
character;
|
||||
if (targetCharacter is null) { return false; }
|
||||
if (!IsViableTarget(targetTypes, targetCharacter)) { return false; }
|
||||
foreach (var conditional in conditionals)
|
||||
{
|
||||
if (abilityCharacter.Character is not Character character) { return false; }
|
||||
if (!IsViableTarget(targetTypes, character)) { return false; }
|
||||
foreach (var conditional in conditionals)
|
||||
{
|
||||
if (!conditional.Matches(character)) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter));
|
||||
return false;
|
||||
if (!conditional.Matches(targetCharacter)) { return false; }
|
||||
}
|
||||
return MatchesCharacter(targetCharacter);
|
||||
}
|
||||
|
||||
protected virtual bool MatchesCharacter(Character character)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,6 +1,6 @@
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
|
||||
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionCharacter
|
||||
{
|
||||
private readonly Identifier identifier;
|
||||
|
||||
@@ -9,11 +9,9 @@ namespace Barotrauma.Abilities
|
||||
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
}
|
||||
|
||||
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
protected override bool MatchesCharacter(Character character)
|
||||
{
|
||||
if (abilityObject is not IAbilityCharacter ability) { return false; }
|
||||
|
||||
return !ability.Character.MarkedAsLooted.Contains(identifier);
|
||||
return character != null &&!character.MarkedAsLooted.Contains(identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -2,15 +2,13 @@
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
|
||||
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionCharacter
|
||||
{
|
||||
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
|
||||
|
||||
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
protected override bool MatchesCharacter(Character character)
|
||||
{
|
||||
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
|
||||
|
||||
return targetCharacter.Character.IsUnconscious;
|
||||
return character is { IsUnconscious: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using Barotrauma.Items.Components;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
class AbilityConditionItemIsStatic : AbilityConditionData
|
||||
{
|
||||
public AbilityConditionItemIsStatic(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
|
||||
|
||||
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
|
||||
{
|
||||
if (abilityObject is IAbilityItem { Item: var item })
|
||||
{
|
||||
return item.GetComponent<Holdable>() is null && item.GetComponent<Wearable>() is null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -1,6 +1,8 @@
|
||||
namespace Barotrauma.Abilities
|
||||
using System;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
class AbilityConditionHasPermanentStat : AbilityConditionDataless
|
||||
class AbilityConditionHasPermanentStat : AbilityConditionCharacter
|
||||
{
|
||||
private readonly Identifier statIdentifier;
|
||||
private readonly StatTypes statType;
|
||||
@@ -21,8 +23,13 @@
|
||||
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
|
||||
}
|
||||
|
||||
protected override bool MatchesConditionSpecific()
|
||||
protected override bool MatchesCharacter(Character character)
|
||||
{
|
||||
if (character?.Info == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in {nameof(AbilityConditionHasPermanentStat.MatchesCharacter)}: character {character} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
return false;
|
||||
}
|
||||
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
|
||||
return character.Info.GetSavedStatValue(statType, identifier) >= min;
|
||||
}
|
||||
|
||||
+5
-8
@@ -2,21 +2,18 @@
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless
|
||||
internal sealed class AbilityConditionLowestLevel : AbilityConditionCharacter
|
||||
{
|
||||
public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
|
||||
|
||||
protected override bool MatchesConditionSpecific()
|
||||
protected override bool MatchesCharacter(Character character)
|
||||
{
|
||||
int ownLevel = character.Info.GetCurrentLevel();
|
||||
|
||||
foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
if (crew == character) { continue; }
|
||||
|
||||
if (crew.Info.GetCurrentLevel() < ownLevel) { return false; }
|
||||
if (otherCharacter == character) { continue; }
|
||||
if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; }
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -38,7 +38,11 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
|
||||
if (abilityObject is AbilityCharacterKill { Killer: { } killer })
|
||||
{
|
||||
ApplyEffectSpecific(killer);
|
||||
}
|
||||
else if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
|
||||
{
|
||||
ApplyEffectSpecific(targetCharacter);
|
||||
}
|
||||
|
||||
+3
-1
@@ -7,12 +7,14 @@ namespace Barotrauma.Abilities
|
||||
private readonly ItemTalentStats stat;
|
||||
private readonly float value;
|
||||
private readonly bool stackable;
|
||||
private readonly bool save;
|
||||
|
||||
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
|
||||
value = abilityElement.GetAttributeFloat("value", 0f);
|
||||
stackable = abilityElement.GetAttributeBool("stackable", true);
|
||||
save = abilityElement.GetAttributeBool("save", false);
|
||||
}
|
||||
|
||||
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
|
||||
@@ -27,7 +29,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (abilityObject is not IAbilityItem ability) { return; }
|
||||
|
||||
ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
|
||||
ability.Item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -10,6 +10,7 @@ namespace Barotrauma.Abilities
|
||||
private readonly float value;
|
||||
private readonly ImmutableHashSet<Identifier> tags;
|
||||
private readonly bool stackable;
|
||||
private readonly bool save;
|
||||
|
||||
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
@@ -17,6 +18,7 @@ namespace Barotrauma.Abilities
|
||||
value = abilityElement.GetAttributeFloat("value", 0f);
|
||||
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
|
||||
stackable = abilityElement.GetAttributeBool("stackable", true);
|
||||
save = abilityElement.GetAttributeBool("save", false);
|
||||
}
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
@@ -44,7 +46,7 @@ namespace Barotrauma.Abilities
|
||||
if (item.Submarine?.TeamID != Character.TeamID) { continue; }
|
||||
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
|
||||
{
|
||||
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
|
||||
item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-8
@@ -1,4 +1,6 @@
|
||||
namespace Barotrauma.Abilities
|
||||
using System;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
public enum PermanentStatPlaceholder
|
||||
{
|
||||
@@ -19,7 +21,12 @@
|
||||
private readonly bool setValue;
|
||||
private readonly PermanentStatPlaceholder placeholder;
|
||||
|
||||
//private readonly float maximumValue;
|
||||
/// <summary>
|
||||
/// If enabled, the effect is applied on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
|
||||
/// Defaults to false (= targets the character who has the talent).
|
||||
/// </summary>
|
||||
private readonly bool targetAbilityTarget = false;
|
||||
|
||||
public override bool AllowClientSimulation => true;
|
||||
public override bool AppliesEffectOnIntervalUpdate => true;
|
||||
|
||||
@@ -40,27 +47,28 @@
|
||||
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
|
||||
setValue = abilityElement.GetAttributeBool("setvalue", false);
|
||||
placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
|
||||
targetAbilityTarget = abilityElement.GetAttributeBool(nameof(targetAbilityTarget), false);
|
||||
}
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
{
|
||||
if (giveOnAddingFirstTime && addingFirstTime)
|
||||
{
|
||||
ApplyEffectSpecific();
|
||||
ApplyEffectSpecific(abilityObject: null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyEffect(AbilityObject abilityObject)
|
||||
{
|
||||
ApplyEffectSpecific();
|
||||
ApplyEffectSpecific(abilityObject);
|
||||
}
|
||||
|
||||
protected override void ApplyEffect()
|
||||
{
|
||||
ApplyEffectSpecific();
|
||||
ApplyEffectSpecific(abilityObject: null);
|
||||
}
|
||||
|
||||
private void ApplyEffectSpecific()
|
||||
private void ApplyEffectSpecific(AbilityObject abilityObject)
|
||||
{
|
||||
Identifier identifier = HandlePlaceholders(placeholder, statIdentifier);
|
||||
if (targetAllies)
|
||||
@@ -72,7 +80,21 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
|
||||
Character targetCharacter =
|
||||
targetAbilityTarget ?
|
||||
(abilityObject as IAbilityCharacter)?.Character ?? Character :
|
||||
Character;
|
||||
if (targetCharacter == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character was null.\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
return;
|
||||
}
|
||||
if (targetCharacter?.Info == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character {targetCharacter} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
|
||||
return;
|
||||
}
|
||||
targetCharacter.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +105,7 @@
|
||||
switch (placeholder)
|
||||
{
|
||||
case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location:
|
||||
return original.Replace("[placeholder]", location.Name);
|
||||
return original.Replace("[placeholder]", location.NameIdentifier.Value);
|
||||
case PermanentStatPlaceholder.LocationIndex:
|
||||
return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString());
|
||||
}
|
||||
|
||||
+4
@@ -44,9 +44,13 @@ namespace Barotrauma.Abilities
|
||||
case "fallbackabilities":
|
||||
LoadFallbackAbilities(subElement);
|
||||
break;
|
||||
case "condition":
|
||||
case "conditions":
|
||||
LoadConditions(subElement);
|
||||
break;
|
||||
default:
|
||||
DebugConsole.ThrowError($"Error in talent {characterTalent.Prefab.Identifier}: unrecognized xml element \"{subElement.Name}\".");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,23 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public Item? FindItemInContainer(ItemContainer? container)
|
||||
=> container?.Inventory.GetItemsAt(Slot).ElementAt(StackIndex);
|
||||
{
|
||||
var items = container?.Inventory.GetItemsAt(Slot);
|
||||
if (items != null && StackIndex >= 0 && StackIndex < items.Count())
|
||||
{
|
||||
return items.ElementAt(StackIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
string errorMsg =
|
||||
$"Circuit box error: failed to find an item in the container {container?.Item.Name ?? "null"}.";
|
||||
DebugConsole.ThrowError(
|
||||
errorMsg +
|
||||
$" Items: {items?.Count().ToString() ?? "null"}, " +
|
||||
$" Slot: {Slot}, StackIndex: {StackIndex}");
|
||||
GameAnalyticsManager.AddErrorEventOnce("ItemSlotIndexPair.FindItemInContainer", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,12 @@ namespace Barotrauma
|
||||
LanguageIdentifier language = languageName.ToLanguageIdentifier();
|
||||
if (!TextManager.TextPacks.ContainsKey(language))
|
||||
{
|
||||
TextManager.TextPacks.TryAdd(language, ImmutableHashSet<TextPack>.Empty);
|
||||
TextManager.TextPacks.TryAdd(language, ImmutableList<TextPack>.Empty);
|
||||
}
|
||||
|
||||
var newPack = new TextPack(this, mainElement, language);
|
||||
var newHashSet = TextManager.TextPacks[language].Add(newPack);
|
||||
var newList = TextManager.TextPacks[language].Add(newPack);
|
||||
TextManager.TextPacks.TryRemove(language, out _);
|
||||
TextManager.TextPacks.TryAdd(language, newHashSet);
|
||||
TextManager.TextPacks.TryAdd(language, newList);
|
||||
TextManager.IncrementLanguageVersion();
|
||||
}
|
||||
|
||||
@@ -33,9 +32,9 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var kvp in TextManager.TextPacks.ToArray())
|
||||
{
|
||||
var newHashSet = kvp.Value.Where(p => p.ContentFile != this).ToImmutableHashSet();
|
||||
var newList = kvp.Value.Where(p => p.ContentFile != this).ToImmutableList();
|
||||
TextManager.TextPacks.TryRemove(kvp.Key, out _);
|
||||
if (newHashSet.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newHashSet); }
|
||||
if (newList.Count != 0) { TextManager.TextPacks.TryAdd(kvp.Key, newList); }
|
||||
}
|
||||
TextManager.IncrementLanguageVersion();
|
||||
if (!TextManager.TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
|
||||
@@ -49,7 +48,11 @@ namespace Barotrauma
|
||||
|
||||
public override void Sort()
|
||||
{
|
||||
//Overrides for text packs don't exist! Should we change this?
|
||||
foreach (var language in TextManager.TextPacks.Keys.ToList())
|
||||
{
|
||||
TextManager.TextPacks[language] =
|
||||
TextManager.TextPacks[language].Sort((t1, t2) => (t1.ContentFile.ContentPackage?.Index ?? int.MaxValue) - (t2.ContentFile.ContentPackage?.Index ?? int.MaxValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ namespace Barotrauma
|
||||
.Distinct(new TypeComparer<ContentFile>())
|
||||
.ForEach(f => f.Sort());
|
||||
MergedHash = Md5Hash.MergeHashes(All.Select(cp => cp.Hash));
|
||||
TextManager.IncrementLanguageVersion();
|
||||
}
|
||||
|
||||
public static int IndexOf(ContentPackage contentPackage)
|
||||
|
||||
@@ -858,7 +858,13 @@ namespace Barotrauma
|
||||
|
||||
commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) =>
|
||||
{
|
||||
if (GameMain.GameSession?.EventManager is EventManager eventManager && args.Length > 0)
|
||||
if (args.Length == 0)
|
||||
{
|
||||
ThrowError($"Please specify the identifier of the event you want to debug.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (GameMain.GameSession?.EventManager is EventManager eventManager)
|
||||
{
|
||||
var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]);
|
||||
if (ev == null)
|
||||
@@ -877,9 +883,18 @@ namespace Barotrauma
|
||||
}
|
||||
}, isCheat: true, getValidArgs: () =>
|
||||
{
|
||||
IEnumerable<EventPrefab> eventPrefabs;
|
||||
if (GameMain.GameSession?.EventManager == null || GameMain.GameSession.EventManager.ActiveEvents.None())
|
||||
{
|
||||
eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
eventPrefabs = GameMain.GameSession.EventManager.ActiveEvents.Select(e => e.Prefab);
|
||||
}
|
||||
return new[]
|
||||
{
|
||||
GameMain.GameSession?.EventManager?.ActiveEvents.Select(ev => ev.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
|
||||
eventPrefabs.Select(ev => ev.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
|
||||
};
|
||||
}));
|
||||
|
||||
@@ -1613,7 +1628,7 @@ namespace Barotrauma
|
||||
int i = 0;
|
||||
foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections)
|
||||
{
|
||||
NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).Name, Color.White);
|
||||
NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White);
|
||||
i++;
|
||||
}
|
||||
ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) =>
|
||||
@@ -1627,7 +1642,7 @@ namespace Barotrauma
|
||||
}
|
||||
Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
|
||||
campaign.Map.SelectLocation(location);
|
||||
NewMessage(location.Name + " selected.", Color.White);
|
||||
NewMessage(location.DisplayName + " selected.", Color.White);
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -1641,7 +1656,7 @@ namespace Barotrauma
|
||||
}
|
||||
Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
|
||||
campaign.Map.SelectLocation(location);
|
||||
NewMessage(location.Name + " selected.", Color.White);
|
||||
NewMessage(location.DisplayName + " selected.", Color.White);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1913,7 +1928,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (location.Stores != null)
|
||||
{
|
||||
var msg = "--- Location: " + location.Name + " ---";
|
||||
var msg = "--- Location: " + location.DisplayName + " ---";
|
||||
foreach (var store in location.Stores)
|
||||
{
|
||||
msg += $"\nStore identifier: {store.Value.Identifier}";
|
||||
@@ -2417,11 +2432,7 @@ namespace Barotrauma
|
||||
|
||||
public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null)
|
||||
{
|
||||
if (contentPackage != null)
|
||||
{
|
||||
string colorStr = XMLExtensions.ToStringHex(Color.MediumPurple);
|
||||
msg = $"‖color:{colorStr}‖[{contentPackage.Name}]‖color:end‖ {msg}";
|
||||
}
|
||||
msg = AddContentPackageInfoToMessage(msg, contentPackage);
|
||||
color ??= Color.Red;
|
||||
NewMessage(msg, color.Value, isCommand: false, isError: true);
|
||||
}
|
||||
@@ -2557,11 +2568,7 @@ namespace Barotrauma
|
||||
|
||||
public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false)
|
||||
{
|
||||
if (contentPackage != null)
|
||||
{
|
||||
string color = XMLExtensions.ToStringHex(Color.MediumPurple);
|
||||
error = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {error}";
|
||||
}
|
||||
error = AddContentPackageInfoToMessage(error, contentPackage);
|
||||
if (e != null)
|
||||
{
|
||||
error += " {" + e.Message + "}\n";
|
||||
@@ -2610,16 +2617,22 @@ namespace Barotrauma
|
||||
|
||||
public static void AddWarning(string warning, ContentPackage contentPackage = null)
|
||||
{
|
||||
warning = $"WARNING: {warning}";
|
||||
if (contentPackage != null)
|
||||
{
|
||||
string color = XMLExtensions.ToStringHex(Color.MediumPurple);
|
||||
warning = $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {warning}";
|
||||
}
|
||||
warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage);
|
||||
System.Diagnostics.Debug.WriteLine(warning);
|
||||
NewMessage(warning, Color.Yellow);
|
||||
}
|
||||
|
||||
private static string AddContentPackageInfoToMessage(string message, ContentPackage contentPackage)
|
||||
{
|
||||
if (contentPackage == null) { return message; }
|
||||
#if CLIENT
|
||||
string color = XMLExtensions.ToStringHex(Color.MediumPurple);
|
||||
return $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {message}";
|
||||
#else
|
||||
return $"[{contentPackage.Name}] {message}";
|
||||
#endif
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
private static IEnumerable<CoroutineStatus> CreateMessageBox(string errorMsg)
|
||||
{
|
||||
|
||||
@@ -159,11 +159,13 @@ namespace Barotrauma
|
||||
OnItemDeconstructedInventory,
|
||||
OnStopTinkering,
|
||||
OnItemPicked,
|
||||
OnItemSelected,
|
||||
OnGeneticMaterialCombinedOrRefined,
|
||||
OnCrewGeneticMaterialCombinedOrRefined,
|
||||
AfterSubmarineAttacked,
|
||||
OnApplyTreatment,
|
||||
OnStatusEffectIdentifier,
|
||||
OnRepairedOutsideLeak
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -550,7 +552,27 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Can be used to prevent certain talents from being unlocked by specifying the talent's identifier via CharacterAbilityGivePermanentStat.
|
||||
/// </summary>
|
||||
LockedTalents
|
||||
LockedTalents,
|
||||
|
||||
/// <summary>
|
||||
/// Used to reduce or increase the cost of hiring certain jobs by a percentage.
|
||||
/// </summary>
|
||||
HireCostMultiplier,
|
||||
|
||||
/// <summary>
|
||||
/// Used to increase how much items can stack in the characters inventory.
|
||||
/// </summary>
|
||||
InventoryExtraStackSize,
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the range of the sounds emitted by the character (can be used to make the character easier or more difficult for monsters to hear)
|
||||
/// </summary>
|
||||
SoundRangeMultiplier,
|
||||
|
||||
/// <summary>
|
||||
/// Modifies how far the character can be seen from (can be used to make the character easier or more difficult for monsters to see)
|
||||
/// </summary>
|
||||
SightRangeMultiplier
|
||||
}
|
||||
|
||||
internal enum ItemTalentStats
|
||||
@@ -565,7 +587,8 @@ namespace Barotrauma
|
||||
ReactorMaxOutput,
|
||||
ReactorFuelConsumption,
|
||||
DeconstructorSpeed,
|
||||
FabricationSpeed
|
||||
FabricationSpeed,
|
||||
ExtraStackSize
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+86
-17
@@ -1,3 +1,6 @@
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -8,7 +11,16 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
private PropertyConditional Conditional { get; }
|
||||
[Serialize(PropertyConditional.LogicalOperatorType.Or, IsPropertySaveable.Yes)]
|
||||
public PropertyConditional.LogicalOperatorType LogicalOperator { get; set; }
|
||||
|
||||
private ImmutableArray<PropertyConditional> Conditionals { get; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier ApplyTagToLinkedHulls { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside when the item is used.")]
|
||||
public Identifier ApplyTagToHull { get; set; }
|
||||
|
||||
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
@@ -17,14 +29,38 @@ namespace Barotrauma
|
||||
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.",
|
||||
contentPackage: parentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
Conditional = PropertyConditional.FromXElement(element, IsNotTargetTagAttribute).FirstOrDefault();
|
||||
if (Conditional == null)
|
||||
var conditionalElements = element.GetChildElements("Conditional");
|
||||
if (conditionalElements.None())
|
||||
{
|
||||
//backwards compatibility
|
||||
Conditionals = PropertyConditional.FromXElement(element, IsConditionalAttribute).ToImmutableArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
var conditionalList = new List<PropertyConditional>();
|
||||
foreach (ContentXElement subElement in conditionalElements)
|
||||
{
|
||||
conditionalList.AddRange(PropertyConditional.FromXElement(subElement));
|
||||
break;
|
||||
}
|
||||
Conditionals = conditionalList.ToImmutableArray();
|
||||
}
|
||||
|
||||
if (Conditionals.None())
|
||||
{
|
||||
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.",
|
||||
contentPackage: parentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
|
||||
static bool IsNotTargetTagAttribute(XAttribute attribute) => attribute.NameAsIdentifier() != "targettag";
|
||||
static bool IsConditionalAttribute(XAttribute attribute)
|
||||
{
|
||||
var nameAsIdentifier = attribute.NameAsIdentifier();
|
||||
return
|
||||
nameAsIdentifier != nameof(TargetTag) &&
|
||||
nameAsIdentifier != nameof(LogicalOperator) &&
|
||||
nameAsIdentifier != nameof(ApplyTagToLinkedHulls) &&
|
||||
nameAsIdentifier != nameof(ApplyTagToHull);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetEventName()
|
||||
@@ -34,32 +70,65 @@ namespace Barotrauma
|
||||
|
||||
protected override bool? DetermineSuccess()
|
||||
{
|
||||
ISerializableEntity target = null;
|
||||
IEnumerable<ISerializableEntity> targets = null;
|
||||
if (!TargetTag.IsEmpty)
|
||||
{
|
||||
foreach (var t in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
if (t is ISerializableEntity e)
|
||||
{
|
||||
target = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
targets = ParentEvent.GetTargets(TargetTag).OfType<ISerializableEntity>();
|
||||
}
|
||||
if (target == null)
|
||||
|
||||
if (targets.None())
|
||||
{
|
||||
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
|
||||
contentPackage: ParentEvent.Prefab.ContentPackage);
|
||||
}
|
||||
if (target == null || Conditional == null)
|
||||
|
||||
if (targets.None() || Conditionals.None())
|
||||
{
|
||||
foreach (var target in targets)
|
||||
{
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool success = false;
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (ConditionalsMatch(target))
|
||||
{
|
||||
success = true;
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ConditionalsMatch(ISerializableEntity target)
|
||||
{
|
||||
if (LogicalOperator == PropertyConditional.LogicalOperatorType.And)
|
||||
{
|
||||
return Conditionals.All(c => ConditionalMatches(target, c));
|
||||
}
|
||||
else
|
||||
{
|
||||
return Conditionals.Any(c => ConditionalMatches(target, c));
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ConditionalMatches(ISerializableEntity target, PropertyConditional conditional)
|
||||
{
|
||||
if (target is Item item)
|
||||
{
|
||||
return item.ConditionalMatches(Conditional);
|
||||
if (!conditional.TargetItemComponent.IsNullOrEmpty() &&
|
||||
item.Components.None(ic => ic.Name == conditional.TargetItemComponent))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return item.ConditionalMatches(conditional);
|
||||
}
|
||||
return Conditional.Matches(target);
|
||||
return conditional.Matches(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-3
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -8,12 +9,18 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check from.")]
|
||||
public Identifier EntityTag { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Entities that also have this tag are excluded.")]
|
||||
public Identifier ExcludedEntityTag { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag of the entity to do the visibility check to.")]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Does the entity need to be facing the target? Only valid if the entity is a character.")]
|
||||
public bool CheckFacing { get; set; }
|
||||
|
||||
[Serialize(1000.0f, IsPropertySaveable.Yes, description: "Maximum distance between the targets.")]
|
||||
public float MaxDistance { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the entity who saw the target when the check succeeds.")]
|
||||
public Identifier ApplyTagToEntity { get; set; }
|
||||
|
||||
@@ -31,11 +38,17 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var entity in ParentEvent.GetTargets(EntityTag))
|
||||
{
|
||||
if (!ExcludedEntityTag.IsEmpty)
|
||||
{
|
||||
if (ParentEvent.GetTargets(ExcludedEntityTag).Contains(entity)) { continue; }
|
||||
}
|
||||
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
if (!AllowSameEntity && entity == target) { continue; }
|
||||
if (Character.IsTargetVisible(target, entity, CheckFacing))
|
||||
{
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, entity.WorldPosition) > MaxDistance * MaxDistance) { continue; }
|
||||
if (Character.IsTargetVisible(target, entity, seeThroughWindows: true, CheckFacing))
|
||||
{
|
||||
if (!ApplyTagToEntity.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToEntity, entity);
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace Barotrauma
|
||||
|
||||
public EventAction(ScriptedEvent parentEvent, ContentXElement element)
|
||||
{
|
||||
ParentEvent = parentEvent ?? throw new ArgumentNullException(nameof(parentEvent));
|
||||
ParentEvent = parentEvent;
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
|
||||
@@ -141,7 +141,11 @@ namespace Barotrauma
|
||||
Identifier typeName = element.Name.ToString().ToIdentifier();
|
||||
if (typeName == "TutorialSegmentAction")
|
||||
{
|
||||
typeName = "EventObjectiveAction".ToIdentifier();
|
||||
typeName = nameof(EventObjectiveAction).ToIdentifier();
|
||||
}
|
||||
else if (typeName == "TutorialHighlightAction")
|
||||
{
|
||||
typeName = nameof(HighlightAction).ToIdentifier();
|
||||
}
|
||||
actionType = Type.GetType("Barotrauma." + typeName, throwOnError: true, ignoreCase: true);
|
||||
if (actionType == null) { throw new NullReferenceException(); }
|
||||
@@ -170,6 +174,30 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected void ApplyTagsToHulls(Entity entity, Identifier hullTag, Identifier linkedHullTag)
|
||||
{
|
||||
var currentHull = entity switch
|
||||
{
|
||||
Item item => item.CurrentHull,
|
||||
Character character => character.CurrentHull,
|
||||
_ => null,
|
||||
};
|
||||
if (currentHull == null) { return; }
|
||||
|
||||
if (!hullTag.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(hullTag, currentHull);
|
||||
}
|
||||
if (!linkedHullTag.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(linkedHullTag, currentHull);
|
||||
foreach (var linkedHull in currentHull.GetLinkedEntities<Hull>())
|
||||
{
|
||||
ParentEvent.AddTarget(linkedHullTag, linkedHull);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rich test to display in debugdraw
|
||||
/// </summary>
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ namespace Barotrauma
|
||||
{
|
||||
partial class EventObjectiveAction : EventAction
|
||||
{
|
||||
public enum SegmentActionType { Trigger, Add, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove };
|
||||
public enum SegmentActionType { Trigger, Add, AddIfNotFound, Complete, CompleteAndRemove, Remove, Fail, FailAndRemove };
|
||||
|
||||
[Serialize(SegmentActionType.Trigger, IsPropertySaveable.Yes)]
|
||||
public SegmentActionType Type { get; set; }
|
||||
|
||||
@@ -5,17 +5,31 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Serialize(-1, IsPropertySaveable.Yes)]
|
||||
public int MaxTimes { get; set; }
|
||||
|
||||
private int counter;
|
||||
|
||||
public GoTo(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
|
||||
public override bool IsFinished(ref string goTo)
|
||||
{
|
||||
goTo = Name;
|
||||
if (counter < MaxTimes || MaxTimes <= 0)
|
||||
{
|
||||
goTo = Name;
|
||||
counter++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToDebugString()
|
||||
{
|
||||
return $"[-] Go to label \"{Name}\"";
|
||||
string msg = $"[-] Go to label \"{Name}\"";
|
||||
if (MaxTimes > 0)
|
||||
{
|
||||
msg += $" ({counter}/{MaxTimes})";
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
public override void Reset() { }
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#nullable enable
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
partial class HighlightAction : EventAction
|
||||
{
|
||||
private static readonly Color highlightColor = Color.Orange;
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Only the player controlling this character will see the highlight. If empty, all players will see it.")]
|
||||
public Identifier TargetCharacter { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes)]
|
||||
public bool State { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
public HighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
var targetCharacters = TargetCharacter.IsEmpty ? null : ParentEvent.GetTargets(TargetCharacter).OfType<Character>();
|
||||
foreach (var target in ParentEvent.GetTargets(TargetTag))
|
||||
{
|
||||
SetHighlightProjSpecific(target, targetCharacters);
|
||||
}
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
partial void SetHighlightProjSpecific(Entity entity, IEnumerable<Character>? targetCharacters);
|
||||
|
||||
public override bool IsFinished(ref string goToLabel) => isFinished;
|
||||
|
||||
public override void Reset() => isFinished = false;
|
||||
}
|
||||
@@ -123,11 +123,11 @@ namespace Barotrauma
|
||||
campaign.Map.Discover(unlockLocation, checkTalents: false);
|
||||
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null)
|
||||
{
|
||||
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\".");
|
||||
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.DisplayName}\".");
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].Name}\" to \"{unlockedMission.Locations[1].Name}\".");
|
||||
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the connection from \"{unlockedMission.Locations[0].DisplayName}\" to \"{unlockedMission.Locations[1].DisplayName}\".");
|
||||
}
|
||||
#if CLIENT
|
||||
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", unlockedMission.Name),
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ namespace Barotrauma
|
||||
public Identifier Type { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public string Name { get; set; }
|
||||
public Identifier Name { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
@@ -77,9 +77,9 @@ namespace Barotrauma
|
||||
location.ChangeType(campaign, locationType);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrEmpty(Name))
|
||||
if (!Name.IsEmpty)
|
||||
{
|
||||
location.ForceName(TextManager.Get(Name).Fallback(Name).Value);
|
||||
location.ForceName(Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Barotrauma
|
||||
WayPoint.WayPointList.Find(wp => wp.Submarine == sub && wp.SpawnType == SpawnType.Human);
|
||||
if (subWaypoint != null)
|
||||
{
|
||||
npc.GiveIdCardTags(subWaypoint, requireSpawnPointTagsNotGiven: false, createNetworkEvent: true);
|
||||
npc.GiveIdCardTags(subWaypoint, createNetworkEvent: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,31 +90,46 @@ namespace Barotrauma
|
||||
|
||||
private void TagPlayers()
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters));
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && c.IsPlayer && (!c.IsIncapacitated || !IgnoreIncapacitatedCharacters));
|
||||
}
|
||||
|
||||
private void TagTraitors()
|
||||
{
|
||||
AddTargetPredicate(Tags.Traitor, e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
|
||||
AddTargetPredicate(
|
||||
Tags.Traitor,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
|
||||
}
|
||||
|
||||
private void TagNonTraitors()
|
||||
{
|
||||
AddTargetPredicate(Tags.NonTraitor, e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
|
||||
AddTargetPredicate(
|
||||
Tags.NonTraitor,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
|
||||
}
|
||||
|
||||
private void TagNonTraitorPlayers()
|
||||
{
|
||||
AddTargetPredicate(Tags.NonTraitorPlayer, e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
|
||||
AddTargetPredicate(
|
||||
Tags.NonTraitorPlayer,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
|
||||
}
|
||||
|
||||
private void TagBots(bool playerCrewOnly)
|
||||
{
|
||||
AddTargetPredicate(Tag, e =>
|
||||
e is Character c &&
|
||||
c.IsBot &&
|
||||
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
|
||||
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Character,
|
||||
e =>
|
||||
e is Character c &&
|
||||
c.IsBot &&
|
||||
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
|
||||
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
|
||||
}
|
||||
|
||||
private void TagCrew()
|
||||
@@ -139,42 +154,67 @@ namespace Barotrauma
|
||||
|
||||
private void TagStructuresByIdentifier(Identifier identifier)
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Structure,
|
||||
e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.Prefab.Identifier == identifier);
|
||||
}
|
||||
|
||||
private void TagStructuresBySpecialTag(Identifier tag)
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag);
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Structure,
|
||||
e => e is Structure s && SubmarineTypeMatches(s.Submarine) && s.SpecialTag.ToIdentifier() == tag);
|
||||
}
|
||||
|
||||
private void TagItemsByIdentifier(Identifier identifier)
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier);
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Item,
|
||||
e => e is Item it && IsValidItem(it) && it.Prefab.Identifier == identifier);
|
||||
}
|
||||
|
||||
private void TagItemsByTag(Identifier tag)
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Item it && IsValidItem(it) && it.HasTag(tag));
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Item,
|
||||
e => e is Item it && IsValidItem(it) && it.HasTag(tag));
|
||||
}
|
||||
|
||||
private void TagHulls()
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine));
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Hull,
|
||||
e => e is Hull h && SubmarineTypeMatches(h.Submarine));
|
||||
}
|
||||
|
||||
private void TagHullsByName(Identifier name)
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Hull,
|
||||
e => e is Hull h && SubmarineTypeMatches(h.Submarine) && h.RoomName.Contains(name.Value, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private void TagSubmarinesByType(Identifier type)
|
||||
{
|
||||
AddTargetPredicate(Tag, e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
|
||||
AddTargetPredicate(
|
||||
Tag,
|
||||
ScriptedEvent.TargetPredicate.EntityType.Submarine,
|
||||
e => e is Submarine s && SubmarineTypeMatches(s) && (type.IsEmpty || type == s.Info?.Type.ToIdentifier()));
|
||||
}
|
||||
|
||||
private bool IsValidItem(Item it)
|
||||
{
|
||||
return (!it.HiddenInGame || AllowHiddenItems) && SubmarineTypeMatches(it.Submarine);
|
||||
return
|
||||
(!it.HiddenInGame || AllowHiddenItems) &&
|
||||
//if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet
|
||||
//= it.Submarine still null
|
||||
SubmarineTypeMatches(it.Submarine ?? it.CurrentHull?.Submarine ?? it.ParentInventory?.Owner?.Submarine);
|
||||
}
|
||||
|
||||
private bool SubmarineTypeMatches(Submarine sub)
|
||||
@@ -197,7 +237,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void AddTargetPredicate(Identifier tag, Predicate<Entity> predicate)
|
||||
private void AddTargetPredicate(Identifier tag, ScriptedEvent.TargetPredicate.EntityType entityType, Predicate<Entity> predicate)
|
||||
{
|
||||
if (ChoosePercentage > 0.0f)
|
||||
{
|
||||
@@ -209,7 +249,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
ParentEvent.AddTargetPredicate(tag, predicate);
|
||||
ParentEvent.AddTargetPredicate(tag, entityType, predicate);
|
||||
mustRecheckTargets = true;
|
||||
}
|
||||
}
|
||||
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
namespace Barotrauma;
|
||||
|
||||
partial class TutorialHighlightAction : EventAction
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier TargetTag { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes)]
|
||||
public bool State { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
public TutorialHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": {nameof(TutorialHighlightAction)} is not supported in multiplayer.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
UpdateProjSpecific();
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific();
|
||||
|
||||
public override bool IsFinished(ref string goToLabel) => isFinished;
|
||||
|
||||
public override void Reset() => isFinished = false;
|
||||
}
|
||||
+15
-16
@@ -30,11 +30,16 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the hull the target item is inside, and all the hulls it's linked to, when the item is used.")]
|
||||
public Identifier ApplyTagToLinkedHulls { get; set; }
|
||||
|
||||
[Serialize(1, IsPropertySaveable.Yes)]
|
||||
public int RequiredUseCount { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
private readonly HashSet<Entity> targets = new HashSet<Entity>();
|
||||
private readonly HashSet<ItemComponent> targetComponents = new HashSet<ItemComponent>();
|
||||
|
||||
private int useCount = 0;
|
||||
|
||||
private Identifier onUseEventIdentifier;
|
||||
private Identifier OnUseEventIdentifier
|
||||
{
|
||||
@@ -58,6 +63,14 @@ namespace Barotrauma
|
||||
|
||||
private void OnItemUsed(Item item, Character user)
|
||||
{
|
||||
if (!UserTag.IsEmpty)
|
||||
{
|
||||
if (!ParentEvent.GetTargets(UserTag).Contains(user)) { return; }
|
||||
}
|
||||
|
||||
useCount++;
|
||||
if (useCount < RequiredUseCount) { return; }
|
||||
|
||||
if (!ApplyTagToItem.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToItem, item);
|
||||
@@ -66,22 +79,7 @@ namespace Barotrauma
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToUser, user);
|
||||
}
|
||||
if (item.CurrentHull != null)
|
||||
{
|
||||
if (!ApplyTagToHull.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToHull, item.CurrentHull);
|
||||
}
|
||||
if (!ApplyTagToLinkedHulls.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToLinkedHulls, item.CurrentHull);
|
||||
foreach (var linkedHull in item.CurrentHull.GetLinkedEntities<Hull>())
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToLinkedHulls, linkedHull);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApplyTagsToHulls(item, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
DeregisterTargets();
|
||||
isFinished = true;
|
||||
}
|
||||
@@ -142,6 +140,7 @@ namespace Barotrauma
|
||||
public override void Reset()
|
||||
{
|
||||
isFinished = false;
|
||||
useCount = 0;
|
||||
DeregisterTargets();
|
||||
}
|
||||
|
||||
|
||||
@@ -412,6 +412,7 @@ namespace Barotrauma
|
||||
preloadedSprites.ForEach(s => s.Remove());
|
||||
preloadedSprites.Clear();
|
||||
|
||||
timeStamps.Clear();
|
||||
|
||||
pathFinder = null;
|
||||
}
|
||||
@@ -905,7 +906,18 @@ namespace Barotrauma
|
||||
activeEvents.Add(QueuedEvents.Dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void EntitySpawned(Entity entity)
|
||||
{
|
||||
foreach (var ev in activeEvents)
|
||||
{
|
||||
if (ev is ScriptedEvent scriptedEvent)
|
||||
{
|
||||
scriptedEvent.EntitySpawned(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateCurrentIntensity(float deltaTime)
|
||||
{
|
||||
intensityUpdateTimer -= deltaTime;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Barotrauma
|
||||
{
|
||||
for (int n = 0; n < 2; n++)
|
||||
{
|
||||
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].Name);
|
||||
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ namespace Barotrauma
|
||||
|
||||
for (int n = 0; n < 2; n++)
|
||||
{
|
||||
string locationName = $"‖color:gui.orange‖{locations[n].Name}‖end‖";
|
||||
string locationName = $"‖color:gui.orange‖{locations[n].DisplayName}‖end‖";
|
||||
if (description != null) { description = description.Replace("[location" + (n + 1) + "]", locationName); }
|
||||
if (successMessage != null) { successMessage = successMessage.Replace("[location" + (n + 1) + "]", locationName); }
|
||||
if (failureMessage != null) { failureMessage = failureMessage.Replace("[location" + (n + 1) + "]", locationName); }
|
||||
@@ -431,8 +431,7 @@ namespace Barotrauma
|
||||
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
|
||||
// use multipliers here so that we can easily add them together without introducing multiplicative XP stacking
|
||||
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f);
|
||||
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
|
||||
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null);
|
||||
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
|
||||
|
||||
DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value));
|
||||
@@ -652,16 +651,18 @@ namespace Barotrauma
|
||||
public Mission Mission { get; set; }
|
||||
}
|
||||
|
||||
class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
|
||||
class AbilityMissionExperienceGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission, IAbilityCharacter
|
||||
{
|
||||
public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier)
|
||||
public AbilityMissionExperienceGainMultiplier(Mission mission, float missionExperienceGainMultiplier, Character character)
|
||||
{
|
||||
Value = missionExperienceGainMultiplier;
|
||||
Mission = mission;
|
||||
Character = character;
|
||||
}
|
||||
|
||||
public float Value { get; set; }
|
||||
public Mission Mission { get; set; }
|
||||
public Character Character { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -673,7 +673,7 @@ namespace Barotrauma
|
||||
monster.AnimController.SetPosition(FarseerPhysics.ConvertUnits.ToSimUnits(pos));
|
||||
|
||||
var eventManager = GameMain.GameSession.EventManager;
|
||||
if (eventManager != null)
|
||||
if (eventManager != null && monster.Params.AI != null)
|
||||
{
|
||||
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath))
|
||||
{
|
||||
@@ -700,7 +700,7 @@ namespace Barotrauma
|
||||
//this will do nothing if the monsters have no swarm behavior defined,
|
||||
//otherwise it'll make the spawned characters act as a swarm
|
||||
SwarmBehavior.CreateSwarm(monsters.Cast<AICharacter>());
|
||||
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI.CombatStrength))}.", Color.LightBlue, debugOnly: true);
|
||||
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI?.CombatStrength ?? 0))}.", Color.LightBlue, debugOnly: true);
|
||||
}
|
||||
|
||||
if (GameMain.GameSession != null)
|
||||
|
||||
@@ -7,7 +7,21 @@ namespace Barotrauma
|
||||
{
|
||||
class ScriptedEvent : Event
|
||||
{
|
||||
private readonly Dictionary<Identifier, List<Predicate<Entity>>> targetPredicates = new Dictionary<Identifier, List<Predicate<Entity>>>();
|
||||
public sealed record TargetPredicate(
|
||||
TargetPredicate.EntityType Type,
|
||||
Predicate<Entity> Predicate)
|
||||
{
|
||||
public enum EntityType
|
||||
{
|
||||
Character,
|
||||
Hull,
|
||||
Item,
|
||||
Structure,
|
||||
Submarine
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<Identifier, List<TargetPredicate>> targetPredicates = new Dictionary<Identifier, List<TargetPredicate>>();
|
||||
|
||||
private readonly Dictionary<Identifier, List<Entity>> cachedTargets = new Dictionary<Identifier, List<Entity>>();
|
||||
|
||||
@@ -17,7 +31,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private readonly Dictionary<Identifier, int> initialAmounts = new Dictionary<Identifier, int>();
|
||||
|
||||
private int prevEntityCount;
|
||||
private bool newEntitySpawned;
|
||||
private int prevPlayerCount, prevBotCount;
|
||||
private Character prevControlled;
|
||||
|
||||
@@ -191,13 +205,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void AddTargetPredicate(Identifier tag, Predicate<Entity> predicate)
|
||||
public void AddTargetPredicate(Identifier tag, TargetPredicate.EntityType entityType, Predicate<Entity> predicate)
|
||||
{
|
||||
if (!targetPredicates.ContainsKey(tag))
|
||||
{
|
||||
targetPredicates.Add(tag, new List<Predicate<Entity>>());
|
||||
targetPredicates.Add(tag, new List<TargetPredicate>());
|
||||
}
|
||||
targetPredicates[tag].Add(predicate);
|
||||
targetPredicates[tag].Add(new TargetPredicate(entityType, predicate));
|
||||
// force re-search for this tag
|
||||
if (cachedTargets.ContainsKey(tag))
|
||||
{
|
||||
@@ -229,7 +243,6 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
List<Entity> targetsToReturn = new List<Entity>();
|
||||
|
||||
if (Targets.ContainsKey(tag))
|
||||
{
|
||||
foreach (Entity e in Targets[tag])
|
||||
@@ -240,11 +253,24 @@ namespace Barotrauma
|
||||
}
|
||||
if (targetPredicates.ContainsKey(tag))
|
||||
{
|
||||
foreach (Entity entity in Entity.GetEntities())
|
||||
foreach (var targetPredicate in targetPredicates[tag])
|
||||
{
|
||||
if (targetPredicates[tag].Any(p => p(entity)) && !targetsToReturn.Contains(entity))
|
||||
IEnumerable<Entity> entityList = targetPredicate.Type switch
|
||||
{
|
||||
targetsToReturn.Add(entity);
|
||||
TargetPredicate.EntityType.Character => Character.CharacterList,
|
||||
TargetPredicate.EntityType.Item => Item.ItemList,
|
||||
TargetPredicate.EntityType.Structure => MapEntity.MapEntityList.Where(m => m is Structure),
|
||||
TargetPredicate.EntityType.Hull => Hull.HullList,
|
||||
TargetPredicate.EntityType.Submarine => Submarine.Loaded,
|
||||
_ => Entity.GetEntities(),
|
||||
};
|
||||
foreach (Entity entity in entityList)
|
||||
{
|
||||
if (targetsToReturn.Contains(entity)) { continue; }
|
||||
if (targetPredicate.Predicate(entity))
|
||||
{
|
||||
targetsToReturn.Add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,14 +319,8 @@ namespace Barotrauma
|
||||
{
|
||||
int botCount = 0;
|
||||
int playerCount = 0;
|
||||
bool forceRefreshTargets = false;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Removed)
|
||||
{
|
||||
forceRefreshTargets = true;
|
||||
continue;
|
||||
}
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
playerCount++;
|
||||
@@ -310,10 +330,11 @@ namespace Barotrauma
|
||||
botCount++;
|
||||
}
|
||||
}
|
||||
if (forceRefreshTargets || Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled)
|
||||
|
||||
if (botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled || NeedsToRefreshCachedTargets())
|
||||
{
|
||||
cachedTargets.Clear();
|
||||
prevEntityCount = Entity.EntityCount;
|
||||
newEntitySpawned = false;
|
||||
prevBotCount = botCount;
|
||||
prevPlayerCount = playerCount;
|
||||
prevControlled = Character.Controlled;
|
||||
@@ -369,6 +390,47 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private bool NeedsToRefreshCachedTargets()
|
||||
{
|
||||
if (newEntitySpawned) { return true; }
|
||||
foreach (var cachedTargetList in cachedTargets.Values)
|
||||
{
|
||||
foreach (var target in cachedTargetList)
|
||||
{
|
||||
//one of the previously cached entities has been removed -> force refresh
|
||||
if (target.Removed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void EntitySpawned(Entity entity)
|
||||
{
|
||||
if (newEntitySpawned) { return; }
|
||||
if (entity is Character character &&
|
||||
Level.Loaded?.StartOutpost != null &&
|
||||
Level.Loaded.StartOutpost.Info.OutpostNPCs.Values.Any(npcList => npcList.Contains(character)))
|
||||
{
|
||||
newEntitySpawned = true;
|
||||
return;
|
||||
}
|
||||
//new entity matches one of the existing predicates -> force refresh
|
||||
foreach (var targetPredicateList in targetPredicates.Values)
|
||||
{
|
||||
foreach (var targetPredicate in targetPredicateList)
|
||||
{
|
||||
if (targetPredicate.Predicate(entity))
|
||||
{
|
||||
newEntitySpawned = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool LevelMeetsRequirements()
|
||||
{
|
||||
if (requiredDestinationTypes == null) { return true; }
|
||||
|
||||
@@ -294,6 +294,11 @@ namespace Barotrauma.Extensions
|
||||
.Where(nullable => nullable.HasValue)
|
||||
.Select(nullable => nullable.Value);
|
||||
|
||||
public static IEnumerable<T> NotNull<T>(this IEnumerable<T> source) where T : class
|
||||
=> source
|
||||
.Where(nullable => nullable != null)
|
||||
.Select(nullable => nullable!);
|
||||
|
||||
public static IEnumerable<T> NotNone<T>(this IEnumerable<Option<T>> source)
|
||||
{
|
||||
foreach (var o in source)
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace Barotrauma
|
||||
while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count)
|
||||
{
|
||||
spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spawnWaypoints == null || !spawnWaypoints.Any())
|
||||
{
|
||||
@@ -306,15 +306,16 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
character.LoadTalents();
|
||||
character.GiveIdCardTags(new List<WayPoint>() { mainSubWaypoints[i], spawnWaypoints[i] });
|
||||
character.Info.StartItemsGiven = true;
|
||||
|
||||
character.GiveIdCardTags(mainSubWaypoints[i]);
|
||||
character.GiveIdCardTags(spawnWaypoints[i]);
|
||||
character.Info.StartItemsGiven = true;
|
||||
if (character.Info.OrderData != null)
|
||||
{
|
||||
character.Info.ApplyOrderData();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AddCharacter(character, sortCrewList: false);
|
||||
#if CLIENT
|
||||
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
|
||||
|
||||
@@ -45,7 +45,14 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo));
|
||||
try
|
||||
{
|
||||
data.Add(identifier, Convert.ChangeType(value, type, NumberFormatInfo.InvariantInfo));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to change the type of the value \"{value}\" to {type}.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Barotrauma
|
||||
if (increase != 0 && Character.Controlled != null)
|
||||
{
|
||||
Character.Controlled.AddMessage(
|
||||
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.Name ?? Faction.Prefab.Name).Value,
|
||||
TextManager.GetWithVariable("reputationgainnotification", "[reputationname]", Location?.DisplayName ?? Faction.Prefab.Name).Value,
|
||||
increase > 0 ? GUIStyle.Green : GUIStyle.Red,
|
||||
playSound: true, Identifier, increase, lifetime: 5.0f);
|
||||
}
|
||||
|
||||
@@ -558,8 +558,8 @@ namespace Barotrauma
|
||||
if (availableTransition == TransitionType.None)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
|
||||
"(current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
|
||||
"(current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
|
||||
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
|
||||
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
|
||||
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
|
||||
@@ -570,8 +570,8 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to load a new campaign level. No available level transitions " +
|
||||
"(transition type: " + availableTransition + ", " +
|
||||
"current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
|
||||
"current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
|
||||
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
|
||||
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
|
||||
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ")\n" +
|
||||
@@ -582,8 +582,8 @@ namespace Barotrauma
|
||||
ShowCampaignUI = ForceMapUI = false;
|
||||
#endif
|
||||
DebugConsole.NewMessage("Transitioning to " + (nextLevel?.Seed ?? "null") +
|
||||
" (current location: " + (map.CurrentLocation?.Name ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.Name ?? "null") + ", " +
|
||||
" (current location: " + (map.CurrentLocation?.DisplayName ?? "null") + ", " +
|
||||
"selected location: " + (map.SelectedLocation?.DisplayName ?? "null") + ", " +
|
||||
"leaving sub: " + (leavingSub?.Info?.Name ?? "null") + ", " +
|
||||
"at start: " + (leavingSub?.AtStartExit.ToString() ?? "null") + ", " +
|
||||
"at end: " + (leavingSub?.AtEndExit.ToString() ?? "null") + ", " +
|
||||
@@ -1024,7 +1024,7 @@ namespace Barotrauma
|
||||
return ToolBox.SelectWeightedRandom(factionsList, weights, random);
|
||||
}
|
||||
|
||||
public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Client client = null)
|
||||
public bool TryHireCharacter(Location location, CharacterInfo characterInfo, Character hirer, Client client = null)
|
||||
{
|
||||
if (characterInfo == null) { return false; }
|
||||
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
|
||||
@@ -1034,7 +1034,8 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!TryPurchase(client, characterInfo.Salary)) { return false; }
|
||||
|
||||
if (!TryPurchase(client, HireManager.GetSalaryFor(characterInfo))) { return false; }
|
||||
characterInfo.IsNewHire = true;
|
||||
characterInfo.Title = null;
|
||||
location.RemoveHireableCharacter(characterInfo);
|
||||
@@ -1263,7 +1264,7 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
|
||||
DebugConsole.NewMessage(" Money: " + Bank.Balance, Color.White);
|
||||
DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.Name, Color.White);
|
||||
DebugConsole.NewMessage(" Current location: " + map.CurrentLocation.DisplayName, Color.White);
|
||||
|
||||
DebugConsole.NewMessage(" Available destinations: ", Color.White);
|
||||
for (int i = 0; i < map.CurrentLocation.Connections.Count; i++)
|
||||
@@ -1271,11 +1272,11 @@ namespace Barotrauma
|
||||
Location destination = map.CurrentLocation.Connections[i].OtherLocation(map.CurrentLocation);
|
||||
if (destination == map.SelectedLocation)
|
||||
{
|
||||
DebugConsole.NewMessage(" " + i + ". " + destination.Name + " [SELECTED]", Color.White);
|
||||
DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName + " [SELECTED]", Color.White);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.NewMessage(" " + i + ". " + destination.Name, Color.White);
|
||||
DebugConsole.NewMessage(" " + i + ". " + destination.DisplayName, Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1307,7 +1308,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (NumberOfMissionsAtLocation(location) > Settings.TotalMaxMissionCount)
|
||||
{
|
||||
DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.Name}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
|
||||
DebugConsole.AddWarning($"Client {sender.Name} had too many missions selected for location {location.DisplayName}! Count was {NumberOfMissionsAtLocation(location)}. Deselecting extra missions.");
|
||||
foreach (Mission mission in currentLocation.SelectedMissions.Where(m => m.Locations[1] == location).Skip(Settings.TotalMaxMissionCount).ToList())
|
||||
{
|
||||
currentLocation.DeselectMission(mission);
|
||||
|
||||
@@ -161,16 +161,18 @@ namespace Barotrauma
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
#if CLIENT
|
||||
case "gamemode": //legacy support
|
||||
case "singleplayercampaign":
|
||||
#if CLIENT
|
||||
CrewManager = new CrewManager(true);
|
||||
var campaign = SinglePlayerCampaign.Load(subElement);
|
||||
campaign.LoadNewLevel();
|
||||
GameMode = campaign;
|
||||
InitOwnedSubs(submarineInfo, ownedSubmarines);
|
||||
break;
|
||||
#else
|
||||
throw new Exception("The server cannot load a single player campaign.");
|
||||
#endif
|
||||
break;
|
||||
case "multiplayercampaign":
|
||||
CrewManager = new CrewManager(false);
|
||||
var mpCampaign = MultiPlayerCampaign.LoadNew(subElement);
|
||||
@@ -567,7 +569,7 @@ namespace Barotrauma
|
||||
if (EndLocation != null && levelData != null)
|
||||
{
|
||||
GUI.AddMessage(levelData.Biome.DisplayName, Color.Lerp(Color.CadetBlue, Color.DarkRed, levelData.Difficulty / 100.0f), 5.0f, playSound: false);
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.Name), Color.CadetBlue, playSound: false);
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Destination"), EndLocation.DisplayName), Color.CadetBlue, playSound: false);
|
||||
var missionsToShow = missions.Where(m => m.Prefab.ShowStartMessage);
|
||||
if (missionsToShow.Count() > 1)
|
||||
{
|
||||
@@ -582,7 +584,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.Name), Color.CadetBlue, playSound: false);
|
||||
GUI.AddMessage(TextManager.AddPunctuation(':', TextManager.Get("Location"), StartLocation.DisplayName), Color.CadetBlue, playSound: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,6 +873,7 @@ namespace Barotrauma
|
||||
|
||||
//Clear the grids to allow for garbage collection
|
||||
Powered.Grids.Clear();
|
||||
Powered.ChangedConnections.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -20,6 +21,23 @@ namespace Barotrauma
|
||||
AvailableCharacters.Remove(character);
|
||||
}
|
||||
|
||||
public static int GetSalaryFor(IReadOnlyCollection<CharacterInfo> hires)
|
||||
{
|
||||
return hires.Sum(hire => GetSalaryFor(hire));
|
||||
}
|
||||
|
||||
public static int GetSalaryFor(CharacterInfo hire)
|
||||
{
|
||||
IEnumerable<Character> crew = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
float multiplier = 0;
|
||||
foreach (var character in crew)
|
||||
{
|
||||
multiplier += character?.Info?.GetSavedStatValueWithAll(StatTypes.HireCostMultiplier, hire.Job.Prefab.Identifier) ?? 0;
|
||||
}
|
||||
float finalMultiplier = 1f + MathF.Max(multiplier, -1f);
|
||||
return (int)(hire.Salary * finalMultiplier);
|
||||
}
|
||||
|
||||
public void GenerateCharacters(Location location, int amount)
|
||||
{
|
||||
AvailableCharacters.ForEach(c => c.Remove());
|
||||
|
||||
@@ -151,6 +151,19 @@ namespace Barotrauma
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public Item FindEquippedItemByTag(Identifier tag)
|
||||
{
|
||||
if (tag.IsEmpty) { return null; }
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
if (SlotTypes[i] == InvSlotType.Any) { continue; }
|
||||
|
||||
var item = slots[i].FirstOrDefault();
|
||||
if (item != null && item.HasTag(tag)) { return item; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int FindLimbSlot(InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
|
||||
@@ -343,7 +343,12 @@ namespace Barotrauma.Items.Components
|
||||
OnFailedToOpen();
|
||||
return;
|
||||
}
|
||||
toggleCooldownTimer = ToggleCoolDown;
|
||||
if (ToggleWhenClicked)
|
||||
{
|
||||
//do not activate cooldown at this point if the door doesn't get toggled when clicked
|
||||
//(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it)
|
||||
toggleCooldownTimer = ToggleCoolDown;
|
||||
}
|
||||
if (IsStuck || IsJammed)
|
||||
{
|
||||
#if CLIENT
|
||||
@@ -404,7 +409,7 @@ namespace Barotrauma.Items.Components
|
||||
position.Y >= item.Rect.Y + Window.Y &&
|
||||
position.Y <= item.Rect.Y + Window.Y + Window.Height &&
|
||||
position.X >= item.Rect.X - maxPerpendicularDistance &&
|
||||
position.Y <= item.Rect.Right + maxPerpendicularDistance;
|
||||
position.X <= item.Rect.Right + maxPerpendicularDistance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,9 +80,6 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public Vector2 OwnerSheetIndex { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public bool SpawnPointTagsGiven { get; set; }
|
||||
|
||||
public IdCard(Item item, ContentXElement element) : base(item, element) { }
|
||||
|
||||
public void Initialize(WayPoint spawnPoint, Character character)
|
||||
|
||||
@@ -439,9 +439,10 @@ namespace Barotrauma.Items.Components
|
||||
if (targetItem.Removed) { return; }
|
||||
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
|
||||
#if CLIENT
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
|
||||
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(targetItem,
|
||||
Character.Controlled.UpdateHUDProgressBar(targetItem,
|
||||
targetItem.WorldPosition,
|
||||
targetItem.Condition / targetItem.MaxCondition,
|
||||
emptyColor: GUIStyle.HealthBarColorLow,
|
||||
|
||||
@@ -200,7 +200,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (requiredTime < float.MaxValue)
|
||||
if (requiredTime < float.MaxValue && picker == Character.Controlled)
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
this,
|
||||
|
||||
@@ -572,8 +572,15 @@ namespace Barotrauma.Items.Components
|
||||
structureFixAmount *= 1 + item.GetQualityModifier(Quality.StatType.RepairToolStructureDamageMultiplier);
|
||||
}
|
||||
|
||||
var didLeak = targetStructure.SectionIsLeakingFromOutside(sectionIndex);
|
||||
|
||||
targetStructure.AddDamage(sectionIndex, -structureFixAmount * degreeOfSuccess, user);
|
||||
|
||||
if (didLeak && !targetStructure.SectionIsLeakingFromOutside(sectionIndex))
|
||||
{
|
||||
user.CheckTalents(AbilityEffectType.OnRepairedOutsideLeak);
|
||||
}
|
||||
|
||||
//if the next section is small enough, apply the effect to it as well
|
||||
//(to make it easier to fix a small "left-over" section)
|
||||
for (int i = -1; i < 2; i += 2)
|
||||
@@ -660,9 +667,10 @@ namespace Barotrauma.Items.Components
|
||||
float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier));
|
||||
levelResource.DeattachTimer += addedDetachTime;
|
||||
#if CLIENT
|
||||
if (targetItem.Prefab.ShowHealthBar)
|
||||
if (targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
|
||||
(user == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(
|
||||
Character.Controlled.UpdateHUDProgressBar(
|
||||
this,
|
||||
targetItem.WorldPosition,
|
||||
levelResource.DeattachTimer / levelResource.DeattachDuration,
|
||||
|
||||
@@ -60,12 +60,19 @@ namespace Barotrauma.Items.Components
|
||||
set
|
||||
{
|
||||
if (parent == value) { return; }
|
||||
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
|
||||
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
|
||||
if (InheritParentIsActive)
|
||||
{
|
||||
if (parent != null) { parent.OnActiveStateChanged -= SetActiveState; }
|
||||
if (value != null) { value.OnActiveStateChanged += SetActiveState; }
|
||||
}
|
||||
parent = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "If this is a child component of another component, should this component inherit the IsActive state of the parent?")]
|
||||
public bool InheritParentIsActive { get; set; }
|
||||
|
||||
public readonly ContentXElement originalElement;
|
||||
|
||||
protected const float CorrectionDelay = 1.0f;
|
||||
@@ -394,8 +401,11 @@ namespace Barotrauma.Items.Components
|
||||
if (ic == null) { break; }
|
||||
|
||||
ic.Parent = this;
|
||||
ic.IsActive = isActive;
|
||||
OnActiveStateChanged += ic.SetActiveState;
|
||||
if (ic.InheritParentIsActive)
|
||||
{
|
||||
ic.IsActive = isActive;
|
||||
OnActiveStateChanged += ic.SetActiveState;
|
||||
}
|
||||
|
||||
item.AddComponent(ic);
|
||||
break;
|
||||
|
||||
@@ -236,6 +236,12 @@ namespace Barotrauma.Items.Components
|
||||
get => Inventory.AllItems.Count(it => it.Condition > 0.0f);
|
||||
}
|
||||
|
||||
public int ExtraStackSize
|
||||
{
|
||||
get => Inventory.ExtraStackSize;
|
||||
set => Inventory.ExtraStackSize = value;
|
||||
}
|
||||
|
||||
private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
|
||||
|
||||
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
||||
@@ -298,7 +304,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
Inventory = new ItemInventory(item, this, totalCapacity, SlotsPerRow);
|
||||
|
||||
|
||||
// we have to assign this here because the fields are serialized before the inventory is created otherwise
|
||||
ExtraStackSize = element.GetAttributeInt(nameof(ExtraStackSize), 0);
|
||||
|
||||
List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
@@ -389,6 +398,7 @@ namespace Barotrauma.Items.Components
|
||||
public void OnItemContained(Item containedItem)
|
||||
{
|
||||
int index = Inventory.FindIndex(containedItem);
|
||||
RelatedItem relatedItem = null;
|
||||
if (index >= 0 && index < slotRestrictions.Length)
|
||||
{
|
||||
if (slotRestrictions[index].ContainableItems != null)
|
||||
@@ -397,6 +407,8 @@ namespace Barotrauma.Items.Components
|
||||
foreach (var containableItem in slotRestrictions[index].ContainableItems)
|
||||
{
|
||||
if (!containableItem.MatchesItem(containedItem)) { continue; }
|
||||
//the 1st matching ContainableItem of the slot determines the hiding, position and rotation of the item
|
||||
relatedItem ??= containableItem;
|
||||
foreach (StatusEffect effect in containableItem.StatusEffects)
|
||||
{
|
||||
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition));
|
||||
@@ -405,7 +417,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
var relatedItem = FindContainableItem(containedItem);
|
||||
var containedItemInfo = new ContainedItem(containedItem,
|
||||
Hide: relatedItem?.Hide ?? false,
|
||||
ItemPos: relatedItem?.ItemPos,
|
||||
@@ -786,12 +797,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private RelatedItem FindContainableItem(Item item)
|
||||
{
|
||||
var relatedItem = ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
|
||||
if (relatedItem == null && AllSubContainableItems != null)
|
||||
{
|
||||
relatedItem = AllSubContainableItems.FirstOrDefault(ci => ci.MatchesItem(item));
|
||||
}
|
||||
return relatedItem;
|
||||
int index = Inventory.FindIndex(item);
|
||||
if (index == -1 ) { return null; }
|
||||
return slotRestrictions[index]?.ContainableItems?.FirstOrDefault(ci => ci.MatchesItem(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1095,6 +1103,7 @@ namespace Barotrauma.Items.Components
|
||||
itemIds[i].Add(idRemap.GetOffsetId(id));
|
||||
}
|
||||
}
|
||||
ExtraStackSize = componentElement.GetAttributeInt(nameof(ExtraStackSize), 0);
|
||||
}
|
||||
|
||||
public override XElement Save(XElement parentElement)
|
||||
@@ -1107,6 +1116,7 @@ namespace Barotrauma.Items.Components
|
||||
itemIdStrings[i] = string.Join(';', items.Select(it => it.ID.ToString()));
|
||||
}
|
||||
componentElement.Add(new XAttribute("contained", string.Join(',', itemIdStrings)));
|
||||
componentElement.Add(new XAttribute(nameof(ExtraStackSize), ExtraStackSize));
|
||||
return componentElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Barotrauma.Items.Components
|
||||
// doesn't quite work properly, remaining time changes if tinkering stops
|
||||
float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease);
|
||||
|
||||
float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
|
||||
float deconstructionSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed);
|
||||
|
||||
if (DeconstructItemsSimultaneously)
|
||||
{
|
||||
|
||||
@@ -149,13 +149,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
|
||||
}
|
||||
currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
|
||||
currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
|
||||
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
|
||||
{
|
||||
currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease;
|
||||
}
|
||||
|
||||
currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce);
|
||||
currForce = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineSpeed, currForce);
|
||||
|
||||
//less effective when in a bad condition
|
||||
currForce *= MathHelper.Lerp(0.5f, 2.0f, condition);
|
||||
|
||||
@@ -469,7 +469,7 @@ namespace Barotrauma.Items.Components
|
||||
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
|
||||
}
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
|
||||
quality = GetFabricatedItemQuality(fabricatedItem, user);
|
||||
quality = GetFabricatedItemQuality(fabricatedItem, user).RollQuality();
|
||||
}
|
||||
|
||||
int amount = (int)fabricationitemAmount.Value;
|
||||
@@ -534,12 +534,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
foreach (Skill skill in fabricatedItem.RequiredSkills)
|
||||
{
|
||||
float userSkill = user.GetSkillLevel(skill.Identifier);
|
||||
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill / Math.Max(userSkill, 1.0f);
|
||||
float addedSkill = skill.Level * SkillSettings.Current.SkillIncreasePerFabricatorRequiredSkill;
|
||||
var addedSkillValue = new AbilityFabricatorSkillGain(skill.Identifier, addedSkill);
|
||||
user.CheckTalents(AbilityEffectType.OnItemFabricationSkillGain, addedSkillValue);
|
||||
|
||||
user.Info.IncreaseSkillLevel(
|
||||
user.Info.ApplySkillGain(
|
||||
skill.Identifier,
|
||||
addedSkillValue.Value);
|
||||
}
|
||||
@@ -576,10 +574,52 @@ namespace Barotrauma.Items.Components
|
||||
return currPowerConsumption;
|
||||
}
|
||||
|
||||
private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
|
||||
public static float CalculateBonusRollPercentage(float skillLevel, float target)
|
||||
=> Math.Clamp((skillLevel - target) / (100f - target) * 100f, min: 0, max: 100);
|
||||
|
||||
public readonly record struct QualityResult(int Quality, float PlusOnePercentage, float PlusTwoPercentage)
|
||||
{
|
||||
if (user?.Info == null) { return 0; }
|
||||
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; }
|
||||
public static readonly QualityResult Empty = new QualityResult(0, 0, 0);
|
||||
|
||||
public bool HasRandomQualityRollChance => PlusOnePercentage > 0f || PlusTwoPercentage > 0f;
|
||||
|
||||
// The total real world percentage for a roll to succeed, taking into account that +1 needs to succeed for +2 to be attempted and
|
||||
// that the chance for only +1 goes down as +2 increases since some of the +1's will turn into +2s
|
||||
public float TotalPlusOnePercentage => Math.Clamp(PlusOnePercentage * (100f - PlusTwoPercentage) / 100f, min: 0, max: 100);
|
||||
public float TotalPlusTwoPercentage => Math.Clamp(PlusOnePercentage * PlusTwoPercentage / 100f, min: 0, max: 100);
|
||||
|
||||
public int RollQuality()
|
||||
{
|
||||
int additionalQuality = 0;
|
||||
if (Roll(PlusOnePercentage))
|
||||
{
|
||||
additionalQuality++;
|
||||
if (Roll(PlusTwoPercentage))
|
||||
{
|
||||
additionalQuality++;
|
||||
}
|
||||
}
|
||||
|
||||
return Quality + additionalQuality;
|
||||
|
||||
static bool Roll(float percentage)
|
||||
=> percentage >= Rand.Range(0, 100, Rand.RandSync.Unsynced);
|
||||
}
|
||||
}
|
||||
|
||||
public const int PlusOneQualityBonusThreshold = 50,
|
||||
PlusTwoQualityBonusThreshold = 75;
|
||||
|
||||
public const int PlusOneTarget = 100,
|
||||
PlusTwoTarget = 125;
|
||||
|
||||
public const float PlusOneLerp = 0.2f,
|
||||
PlusTwoLerp = 0.4f;
|
||||
|
||||
private static QualityResult GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user)
|
||||
{
|
||||
if (user?.Info == null) { return QualityResult.Empty; }
|
||||
if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return QualityResult.Empty; }
|
||||
int quality = 0;
|
||||
float floatQuality = 0.0f;
|
||||
floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality, includeSaved: false);
|
||||
@@ -593,34 +633,63 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
quality = (int)floatQuality;
|
||||
|
||||
const int MaxCraftingSkill = 100;
|
||||
// Use Option here instead of 0 because we want the lowest value and a value of 0 would always be lower than any other chance
|
||||
Option<float> plusOne = Option.None,
|
||||
plusTwo = Option.None;
|
||||
|
||||
//having a higher-than-100 skill (e.g. due to talents) gives +1 quality
|
||||
quality += fabricatedItem.RequiredSkills.All(s => user.GetSkillLevel(s.Identifier) >= MaxCraftingSkill) ? 1 : 0;
|
||||
foreach (var skill in fabricatedItem.RequiredSkills)
|
||||
{
|
||||
//+1 quality if the character's skill level is >20% from the min requirement towards max skill
|
||||
//e.g. if the skill requirement is 10 -> 28
|
||||
//40 -> 52
|
||||
//90 -> 92
|
||||
float skillRequirement = MathHelper.Lerp(skill.Level, MaxCraftingSkill, 0.2f);
|
||||
if (user.GetSkillLevel(skill.Identifier) > skillRequirement)
|
||||
float skillLevel = user.GetSkillLevel(skill.Identifier);
|
||||
|
||||
if (skillLevel >= PlusOneQualityBonusThreshold)
|
||||
{
|
||||
quality += 1;
|
||||
//+1 quality chance if the character's skill level is >20% from the min requirement towards max skill as well as higher than 50
|
||||
//e.g. if the skill requirement is 10 -> 28 (but minimum 50 threshold)
|
||||
//40 -> 52
|
||||
//90 -> 92
|
||||
var bonusChance1 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusOneTarget, PlusOneLerp));
|
||||
plusOne = OverrideChanceIfLess(plusOne, bonusChance1);
|
||||
|
||||
if (skillLevel >= PlusTwoQualityBonusThreshold)
|
||||
{
|
||||
var bonusChance2 = CalculateBonusRollPercentage(skillLevel, MathHelper.Lerp(skill.Level, PlusTwoTarget, PlusTwoLerp));
|
||||
plusTwo = OverrideChanceIfLess(plusTwo, bonusChance2);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
static Option<float> OverrideChanceIfLess(Option<float> original, float bonusChance)
|
||||
{
|
||||
if (original.TryUnwrap(out var originalChance))
|
||||
{
|
||||
return originalChance > bonusChance ? Option.Some(bonusChance) : original;
|
||||
}
|
||||
|
||||
return Option.Some(bonusChance);
|
||||
}
|
||||
}
|
||||
return quality;
|
||||
|
||||
return new QualityResult(quality,
|
||||
PlusOnePercentage: plusOne.Match(some: static f => f, none: static () => 0f),
|
||||
PlusTwoPercentage: plusTwo.Match(some: static f => f, none: static () => 0f));
|
||||
}
|
||||
|
||||
partial void UpdateRequiredTimeProjSpecific();
|
||||
|
||||
private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
|
||||
{
|
||||
return
|
||||
return
|
||||
(user != null && user.HasRecipeForItem(item.Identifier)) ||
|
||||
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
|
||||
}
|
||||
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
|
||||
{
|
||||
if (fabricableItem == null) { return false; }
|
||||
@@ -698,7 +767,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
//fabricating takes 100 times longer if degree of success is close to 0
|
||||
//characters with a higher skill than required can fabricate up to 100% faster
|
||||
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
|
||||
float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f);
|
||||
|
||||
if (user?.Info is { } info && fabricableItem.TargetItem is { } it)
|
||||
{
|
||||
|
||||
@@ -138,14 +138,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
|
||||
|
||||
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
|
||||
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
|
||||
|
||||
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
|
||||
{
|
||||
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
|
||||
}
|
||||
|
||||
currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow);
|
||||
currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow);
|
||||
|
||||
//less effective when in a bad condition
|
||||
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
@@ -875,7 +875,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
|
||||
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
|
||||
private float GetMaxOutput() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
|
||||
private float GetFuelConsumption() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,8 +336,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
showIceSpireWarning = false;
|
||||
if (user != null && user.Info != null &&
|
||||
user.SelectedItem == item &&
|
||||
controlledSub != null && controlledSub.Velocity.LengthSquared() > 0.01f)
|
||||
user.SelectedItem == item)
|
||||
{
|
||||
IncreaseSkillLevel(user, deltaTime);
|
||||
}
|
||||
@@ -402,14 +401,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void IncreaseSkillLevel(Character user, float deltaTime)
|
||||
{
|
||||
if (controlledSub == null) { return; }
|
||||
if (controlledSub.Velocity.LengthSquared() < 0.01f) { return; }
|
||||
if (user?.Info == null) { return; }
|
||||
// Do not increase the helm skill when "steering" the sub while docked into something static (e.g. outpost or wreck)
|
||||
if (GameMain.GameSession?.Campaign != null && controlledSub != null && controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
|
||||
if (GameMain.GameSession?.Campaign != null&& controlledSub.DockedTo.Any(d => d.PhysicsBody.BodyType == BodyType.Static)) { return; }
|
||||
|
||||
float userSkill = Math.Max(user.GetSkillLevel("helm"), 1.0f) / 100.0f;
|
||||
user.Info.IncreaseSkillLevel(
|
||||
"helm".ToIdentifier(),
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenSteering / userSkill * deltaTime);
|
||||
float speedMultiplier = MathHelper.Clamp(TargetVelocity.Length() / 100.0f, 0.0f, 1.0f);
|
||||
user.Info.ApplySkillGain(Tags.HelmSkill,
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenSteering * speedMultiplier * deltaTime);
|
||||
}
|
||||
|
||||
private void UpdateAutoPilot(float deltaTime)
|
||||
|
||||
@@ -395,6 +395,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity);
|
||||
public float GetCapacity() => item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.BatteryCapacity, Capacity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +454,7 @@ namespace Barotrauma.Items.Components
|
||||
base.RemoveComponentSpecific();
|
||||
connectedRecipients?.Clear();
|
||||
connectionDirty?.Clear();
|
||||
recipientsToRefresh.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,9 +1017,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
|
||||
#if CLIENT
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar)
|
||||
if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar && Character.Controlled != null &&
|
||||
(User == Character.Controlled || Character.Controlled.CanSeeTarget(item)))
|
||||
{
|
||||
Character.Controlled?.UpdateHUDProgressBar(targetItem,
|
||||
Character.Controlled.UpdateHUDProgressBar(targetItem,
|
||||
targetItem.WorldPosition,
|
||||
targetItem.Condition / targetItem.MaxCondition,
|
||||
emptyColor: GUIStyle.HealthBarColorLow,
|
||||
|
||||
@@ -495,9 +495,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
foreach (Skill skill in requiredSkills)
|
||||
{
|
||||
float characterSkillLevel = CurrentFixer.GetSkillLevel(skill.Identifier);
|
||||
CurrentFixer.Info?.IncreaseSkillLevel(skill.Identifier,
|
||||
SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f));
|
||||
CurrentFixer.Info?.ApplySkillGain(skill.Identifier, SkillSettings.Current.SkillIncreasePerRepair);
|
||||
}
|
||||
SteamAchievementManager.OnItemRepaired(item, CurrentFixer);
|
||||
CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item));
|
||||
@@ -570,7 +568,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (item.ConditionPercentage > MinDeteriorationCondition)
|
||||
{
|
||||
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
|
||||
float deteriorationSpeed = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
|
||||
if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); }
|
||||
item.Condition -= deteriorationSpeed * deltaTime;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ namespace Barotrauma.Items.Components
|
||||
delayedElementToLoad = Option.None;
|
||||
}
|
||||
|
||||
private void LoadFromXML(ContentXElement loadElement)
|
||||
public void LoadFromXML(ContentXElement loadElement)
|
||||
{
|
||||
foreach (var subElement in loadElement.Elements())
|
||||
{
|
||||
|
||||
@@ -383,6 +383,12 @@ namespace Barotrauma.Items.Components
|
||||
wire.RemoveConnection(item);
|
||||
}
|
||||
}
|
||||
c.Grid = null;
|
||||
}
|
||||
foreach (var connection in Connections)
|
||||
{
|
||||
Powered.ChangedConnections.Remove(connection);
|
||||
connection.Recipients.Clear();
|
||||
}
|
||||
Connections.Clear();
|
||||
|
||||
|
||||
@@ -305,16 +305,17 @@ namespace Barotrauma.Items.Components
|
||||
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
|
||||
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
|
||||
{
|
||||
if (item.body != null && !item.body.Enabled)
|
||||
{
|
||||
lightBrightness = 0.0f;
|
||||
SetLightSourceState(false, 0.0f);
|
||||
}
|
||||
else
|
||||
if (item.body == null || item.body.Enabled ||
|
||||
(item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems))
|
||||
{
|
||||
lightBrightness = 1.0f;
|
||||
SetLightSourceState(true, lightBrightness);
|
||||
}
|
||||
else
|
||||
{
|
||||
lightBrightness = 0.0f;
|
||||
SetLightSourceState(false, 0.0f);
|
||||
}
|
||||
isOn = true;
|
||||
SetLightSourceTransformProjSpecific();
|
||||
base.IsActive = false;
|
||||
@@ -341,8 +342,22 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
Light.ParentSub = item.Submarine;
|
||||
#endif
|
||||
|
||||
|
||||
bool visibleInContainer;
|
||||
var ownerCharacter = item.GetRootInventoryOwner() as Character;
|
||||
if ((item.Container != null && ownerCharacter == null) ||
|
||||
if (ownerCharacter != null && item.RootContainer?.GetComponent<Holdable>() is not { IsActive: true })
|
||||
{
|
||||
//if the item is in a character inventory, the light should only be visible if the character is holding the item
|
||||
//(not if it's e.q. inside a wearable item, or in a rifle worn on the back)
|
||||
visibleInContainer = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
visibleInContainer = item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) == null;
|
||||
}
|
||||
|
||||
if ((item.Container != null && !visibleInContainer && ownerCharacter == null) ||
|
||||
(ownerCharacter != null && ownerCharacter.InvisibleTimer > 0.0f))
|
||||
{
|
||||
lightBrightness = 0.0f;
|
||||
@@ -352,7 +367,7 @@ namespace Barotrauma.Items.Components
|
||||
SetLightSourceTransformProjSpecific();
|
||||
|
||||
PhysicsBody body = ParentBody ?? item.body;
|
||||
if (body != null && !body.Enabled)
|
||||
if (body != null && !body.Enabled && !visibleInContainer)
|
||||
{
|
||||
lightBrightness = 0.0f;
|
||||
SetLightSourceState(false, 0.0f);
|
||||
@@ -432,6 +447,11 @@ namespace Barotrauma.Items.Components
|
||||
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
|
||||
}
|
||||
|
||||
public override void Drop(Character dropper, bool setTransform = true)
|
||||
{
|
||||
SetLightSourceTransform();
|
||||
}
|
||||
|
||||
partial void SetLightSourceState(bool enabled, float brightness);
|
||||
|
||||
public void SetLightSourceTransform()
|
||||
|
||||
@@ -275,6 +275,15 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
if (PhysicsBody != null)
|
||||
{
|
||||
PhysicsBody.Remove();
|
||||
PhysicsBody = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
base.ReceiveSignal(signal, connection);
|
||||
|
||||
@@ -61,6 +61,22 @@ namespace Barotrauma.Items.Components
|
||||
private const float CrewAiFindTargetMaxInterval = 1.0f;
|
||||
private const float CrewAIFindTargetMinInverval = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Bots consider the projectile to move at least this fast when calculating how far ahead a moving target they need to aim.
|
||||
/// Aiming ahead doesn't work reliably with very slow projectiles, because we'd need to take into account drag and gravity,
|
||||
/// and the target would most likely move in a different direction anyway before the projectile reaches it.
|
||||
/// </summary>
|
||||
private const float MinimumProjectileVelocityForAimAhead = 20.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Bots don't try to aim ahead a moving target by more than this amount. If the target is very fast and/or the projectile very slow,
|
||||
/// we'd need to aim so far ahead it'd most likely fail anyway.
|
||||
/// </summary>
|
||||
private const float MaximumAimAhead = 10.0f;
|
||||
|
||||
private float projectileSpeed;
|
||||
private Item previousAmmo;
|
||||
|
||||
private int currentLoaderIndex;
|
||||
|
||||
private const float TinkeringPowerCostReduction = 0.2f;
|
||||
@@ -563,8 +579,9 @@ namespace Barotrauma.Items.Components
|
||||
// Do not increase the weapons skill when operating a turret in an outpost level
|
||||
if (user?.Info != null && (GameMain.GameSession?.Campaign == null || !Level.IsLoadedFriendlyOutpost))
|
||||
{
|
||||
user.Info.IncreaseSkillLevel("weapons".ToIdentifier(),
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime / Math.Max(user.GetSkillLevel("weapons"), 1.0f));
|
||||
user.Info.ApplySkillGain(
|
||||
Tags.WeaponsSkill,
|
||||
SkillSettings.Current.SkillIncreasePerSecondWhenOperatingTurret * deltaTime);
|
||||
}
|
||||
|
||||
float rotMidDiff = MathHelper.WrapAngle(rotation - (minRotation + maxRotation) / 2.0f);
|
||||
@@ -664,6 +681,11 @@ namespace Barotrauma.Items.Components
|
||||
return GetAvailableInstantaneousBatteryPower() >= GetPowerRequiredToShoot();
|
||||
}
|
||||
|
||||
private Vector2 GetBarrelDir()
|
||||
{
|
||||
return new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
|
||||
}
|
||||
|
||||
private bool TryLaunch(float deltaTime, Character character = null, bool ignorePower = false)
|
||||
{
|
||||
tryingToCharge = true;
|
||||
@@ -709,7 +731,8 @@ namespace Barotrauma.Items.Components
|
||||
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
|
||||
if (projectileContainer != null && projectileContainer.Item != item)
|
||||
{
|
||||
projectileContainer?.Item.Use(deltaTime);
|
||||
//user needs to be null because the ammo boxes shouldn't be directly usable by characters
|
||||
projectileContainer?.Item.Use(deltaTime, user: null, userForOnUsedEvent: user);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -735,7 +758,7 @@ namespace Barotrauma.Items.Components
|
||||
ItemContainer projectileContainer = containerItem.GetComponent<ItemContainer>();
|
||||
if (projectileContainer != null)
|
||||
{
|
||||
containerItem.Use(deltaTime);
|
||||
containerItem.Use(deltaTime, user: null, userForOnUsedEvent: user);
|
||||
projectiles = GetLoadedProjectiles();
|
||||
if (projectiles.Any()) { return true; }
|
||||
}
|
||||
@@ -930,6 +953,7 @@ namespace Barotrauma.Items.Components
|
||||
Projectile projectileComponent = projectile.GetComponent<Projectile>();
|
||||
if (projectileComponent != null)
|
||||
{
|
||||
TryDetermineProjectileSpeed(projectileComponent);
|
||||
projectileComponent.Launcher = item;
|
||||
projectileComponent.Attacker = projectileComponent.User = user;
|
||||
if (projectileComponent.Attack != null)
|
||||
@@ -960,6 +984,16 @@ namespace Barotrauma.Items.Components
|
||||
LaunchProjSpecific();
|
||||
}
|
||||
|
||||
private void TryDetermineProjectileSpeed(Projectile projectile)
|
||||
{
|
||||
if (projectile != null && !projectile.Hitscan)
|
||||
{
|
||||
projectileSpeed =
|
||||
ConvertUnits.ToDisplayUnits(
|
||||
MathHelper.Clamp((projectile.LaunchImpulse + LaunchImpulse) / projectile.Item.body.Mass, MinimumProjectileVelocityForAimAhead, NetConfig.MaxPhysicsBodyVelocity));
|
||||
}
|
||||
}
|
||||
|
||||
partial void LaunchProjSpecific();
|
||||
|
||||
private static void ShiftItemsInProjectileContainer(ItemContainer container)
|
||||
@@ -1143,7 +1177,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (target is Hull targetHull)
|
||||
{
|
||||
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
|
||||
Vector2 barrelDir = GetBarrelDir();
|
||||
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
|
||||
{
|
||||
return;
|
||||
@@ -1244,8 +1278,24 @@ namespace Barotrauma.Items.Components
|
||||
if (container != null)
|
||||
{
|
||||
maxProjectileCount += container.Capacity;
|
||||
int projectiles = projectileContainer.ContainedItems.Count(it => it.Condition > 0.0f);
|
||||
usableProjectileCount += projectiles;
|
||||
var projectiles = projectileContainer.ContainedItems.Where(it => it.Condition > 0.0f);
|
||||
var firstProjectile = projectiles.FirstOrDefault();
|
||||
|
||||
if (firstProjectile?.Prefab != previousAmmo?.Prefab)
|
||||
{
|
||||
//assume the projectiles are infinitely fast (no aiming ahead of the target) if we can't find projectiles to calculate the speed based on,
|
||||
//and if the projectile type isn't the same as before
|
||||
projectileSpeed = float.PositiveInfinity;
|
||||
}
|
||||
previousAmmo = firstProjectile;
|
||||
if (projectiles.Any())
|
||||
{
|
||||
var projectile =
|
||||
firstProjectile.GetComponent<Projectile>() ??
|
||||
firstProjectile.ContainedItems.FirstOrDefault()?.GetComponent<Projectile>();
|
||||
TryDetermineProjectileSpeed(projectile);
|
||||
usableProjectileCount += projectiles.Count();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1409,6 +1459,7 @@ namespace Barotrauma.Items.Components
|
||||
targetPos = currentTarget.WorldPosition;
|
||||
}
|
||||
bool iceSpireSpotted = false;
|
||||
Vector2 targetVelocity = Vector2.Zero;
|
||||
// Adjust the target character position (limb or submarine)
|
||||
if (currentTarget is Character targetCharacter)
|
||||
{
|
||||
@@ -1424,20 +1475,39 @@ namespace Barotrauma.Items.Components
|
||||
else
|
||||
{
|
||||
// Target the closest limb. Doesn't make much difference with smaller creatures, but enables the bots to shoot longer abyss creatures like the endworm. Otherwise they just target the main body = head.
|
||||
float closestDist = closestDistance;
|
||||
float closestDistSqr = closestDistance;
|
||||
foreach (Limb limb in targetCharacter.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; }
|
||||
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
|
||||
if (dist < closestDist)
|
||||
float distSqr = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
|
||||
if (distSqr < closestDistSqr)
|
||||
{
|
||||
closestDist = dist;
|
||||
closestDistSqr = distSqr;
|
||||
if (limb == targetCharacter.AnimController.MainLimb)
|
||||
{
|
||||
//prefer main limb (usually a much better target than the extremities that are often the closest limbs)
|
||||
closestDistSqr *= 0.5f;
|
||||
}
|
||||
targetPos = limb.WorldPosition;
|
||||
}
|
||||
}
|
||||
if (closestDist > shootDistance * shootDistance)
|
||||
if (projectileSpeed < float.PositiveInfinity && targetPos.HasValue)
|
||||
{
|
||||
//lead the target (aim where the target will be in the future)
|
||||
float dist = MathF.Sqrt(closestDistSqr);
|
||||
float projectileMovementTime = dist / projectileSpeed;
|
||||
|
||||
targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
|
||||
Vector2 movementAmount = targetVelocity * projectileMovementTime;
|
||||
//don't try to compensate more than 10 meters - if the target is so fast or the projectile so slow we need to go beyond that,
|
||||
//it'd most likely fail anyway
|
||||
movementAmount = ConvertUnits.ToDisplayUnits(movementAmount.ClampLength(MaximumAimAhead));
|
||||
Vector2 futurePosition = targetPos.Value + movementAmount;
|
||||
targetPos = Vector2.Lerp(targetPos.Value, futurePosition, DegreeOfSuccess(character));
|
||||
}
|
||||
if (closestDistSqr > shootDistance * shootDistance)
|
||||
{
|
||||
aiFindTargetTimer = CrewAIFindTargetMinInverval;
|
||||
ResetTarget();
|
||||
@@ -1512,7 +1582,9 @@ 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 CLIENT
|
||||
debugDrawTargetPos = targetPos.Value;
|
||||
#endif
|
||||
if (closestEnemy != null && character.AIController.SelectedAiTarget != closestEnemy.AiTarget)
|
||||
{
|
||||
if (character.IsOnPlayerTeam)
|
||||
@@ -1563,8 +1635,28 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (IsPointingTowards(targetPos.Value))
|
||||
{
|
||||
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
|
||||
Vector2 barrelDir = GetBarrelDir();
|
||||
Vector2 aimStartPos = item.WorldPosition;
|
||||
Vector2 aimEndPos = item.WorldPosition + barrelDir * shootDistance;
|
||||
bool allowShootingIfNothingInWay = false;
|
||||
if (currentTarget != null)
|
||||
{
|
||||
Vector2 targetStartPos = currentTarget.WorldPosition;
|
||||
Vector2 targetEndPos = currentTarget.WorldPosition + targetVelocity * ConvertUnits.ToDisplayUnits(MaximumAimAhead);
|
||||
|
||||
//if there's nothing in the way (not even the target we're trying to aim towards),
|
||||
//shooting should only be allowed if we're aiming ahead of the target, in which case it's to be expected that we're aiming at "thin air"
|
||||
allowShootingIfNothingInWay =
|
||||
targetVelocity.LengthSquared() > 0.001f &&
|
||||
MathUtils.LineSegmentsIntersect(
|
||||
aimStartPos, aimEndPos,
|
||||
targetStartPos, targetEndPos) &&
|
||||
//target needs to be moving roughly perpendicular to us for aiming ahead of it to make sense
|
||||
Math.Abs(Vector2.Dot(Vector2.Normalize(aimEndPos - aimStartPos), Vector2.Normalize(targetEndPos - targetStartPos))) < 0.5f;
|
||||
}
|
||||
|
||||
Vector2 start = ConvertUnits.ToSimUnits(aimStartPos);
|
||||
Vector2 end = ConvertUnits.ToSimUnits(aimEndPos);
|
||||
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
|
||||
Body worldTarget = CheckLineOfSight(start, end);
|
||||
if (closestEnemy != null && closestEnemy.Submarine != null)
|
||||
@@ -1572,11 +1664,13 @@ namespace Barotrauma.Items.Components
|
||||
start -= closestEnemy.Submarine.SimPosition;
|
||||
end -= closestEnemy.Submarine.SimPosition;
|
||||
Body transformedTarget = CheckLineOfSight(start, end);
|
||||
canShoot = CanShoot(transformedTarget, character) && (worldTarget == null || CanShoot(worldTarget, character));
|
||||
canShoot =
|
||||
CanShoot(transformedTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay) &&
|
||||
(worldTarget == null || CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay));
|
||||
}
|
||||
else
|
||||
{
|
||||
canShoot = CanShoot(worldTarget, character);
|
||||
canShoot = CanShoot(worldTarget, character, allowShootingIfNothingInWay: allowShootingIfNothingInWay);
|
||||
}
|
||||
if (!canShoot) { return false; }
|
||||
if (character.IsOnPlayerTeam)
|
||||
@@ -1666,9 +1760,13 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true)
|
||||
private bool CanShoot(Body targetBody, Character user = null, Identifier friendlyTag = default, bool targetSubmarines = true, bool allowShootingIfNothingInWay = false)
|
||||
{
|
||||
if (targetBody == null) { return false; }
|
||||
if (targetBody == null)
|
||||
{
|
||||
//nothing in the way (not even the target we're trying to shoot) -> no point in firing at thin air
|
||||
return allowShootingIfNothingInWay;
|
||||
}
|
||||
Character targetCharacter = null;
|
||||
if (targetBody.UserData is Character c)
|
||||
{
|
||||
|
||||
@@ -559,6 +559,7 @@ namespace Barotrauma.Items.Components
|
||||
foreach (WearableSprite wearableSprite in wearableSprites)
|
||||
{
|
||||
wearableSprite?.Sprite?.Remove();
|
||||
wearableSprite.Picker = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,11 +230,19 @@ namespace Barotrauma
|
||||
|
||||
protected readonly int capacity;
|
||||
protected readonly ItemSlot[] slots;
|
||||
|
||||
|
||||
public bool Locked;
|
||||
|
||||
protected float syncItemsDelay;
|
||||
|
||||
|
||||
private int extraStackSize;
|
||||
public int ExtraStackSize
|
||||
{
|
||||
get => extraStackSize;
|
||||
set => extraStackSize = MathHelper.Max(value, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list.
|
||||
/// </summary>
|
||||
|
||||
@@ -383,7 +383,7 @@ namespace Barotrauma
|
||||
|
||||
public float RotationRad { get; private set; }
|
||||
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, MinValueFloat = 0.0f, MaxValueFloat = 360.0f, DecimalCount = 1, ValueStep = 1f), Serialize(0.0f, IsPropertySaveable.Yes)]
|
||||
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowRotating, DecimalCount = 3, ForceShowPlusMinusButtons = true, ValueStep = 0.1f), Serialize(0.0f, IsPropertySaveable.Yes)]
|
||||
public float Rotation
|
||||
{
|
||||
get
|
||||
@@ -393,7 +393,7 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
if (!Prefab.AllowRotatingInEditor) { return; }
|
||||
RotationRad = MathHelper.ToRadians(value);
|
||||
RotationRad = MathUtils.WrapAnglePi(MathHelper.ToRadians(value));
|
||||
#if CLIENT
|
||||
if (Screen.Selected == GameMain.SubEditorScreen)
|
||||
{
|
||||
@@ -1327,8 +1327,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (FlippedX) clone.FlipX(false);
|
||||
if (FlippedY) clone.FlipY(false);
|
||||
if (FlippedX) { clone.FlipX(false); }
|
||||
if (FlippedY) { clone.FlipY(false); }
|
||||
|
||||
// Flipping an item tampers with its rotation, so restore it
|
||||
clone.Rotation = Rotation;
|
||||
|
||||
foreach (ItemComponent component in clone.components)
|
||||
{
|
||||
@@ -1640,6 +1643,9 @@ namespace Barotrauma
|
||||
return transformedRect;
|
||||
}
|
||||
|
||||
public override Quad2D GetTransformedQuad()
|
||||
=> Quad2D.FromSubmarineRectangle(rect).Rotated(-RotationRad);
|
||||
|
||||
/// <summary>
|
||||
/// goes through every item and re-checks which hull they are in
|
||||
/// </summary>
|
||||
@@ -2516,7 +2522,7 @@ namespace Barotrauma
|
||||
|
||||
if (Prefab.AllowRotatingInEditor)
|
||||
{
|
||||
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
|
||||
RotationRad = MathUtils.WrapAnglePi(-RotationRad);
|
||||
}
|
||||
#if CLIENT
|
||||
if (Prefab.CanSpriteFlipX)
|
||||
@@ -2543,6 +2549,10 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (Prefab.AllowRotatingInEditor)
|
||||
{
|
||||
RotationRad = MathUtils.WrapAngleTwoPi(-RotationRad);
|
||||
}
|
||||
#if CLIENT
|
||||
if (Prefab.CanSpriteFlipY)
|
||||
{
|
||||
@@ -3043,7 +3053,10 @@ namespace Barotrauma
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null)
|
||||
/// <param name="userForOnUsedEvent">User to pass to the OnUsed event. May need to be different than the user in cases like loaders using ammo boxes:
|
||||
/// the box is technically being used by the loader, and doesn't allow a character to use it, but we may still need to know which character caused
|
||||
/// the box to be used.</param>
|
||||
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null, Character userForOnUsedEvent = null)
|
||||
{
|
||||
if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
|
||||
{
|
||||
@@ -3068,7 +3081,7 @@ namespace Barotrauma
|
||||
ic.PlaySound(ActionType.OnUse, user);
|
||||
#endif
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user);
|
||||
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user));
|
||||
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user ?? userForOnUsedEvent));
|
||||
if (ic.DeleteOnUse) { remove = true; }
|
||||
}
|
||||
}
|
||||
@@ -3526,7 +3539,26 @@ namespace Barotrauma
|
||||
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
if (!CanClientAccess(sender) || !(property.GetAttribute<ConditionallyEditable>()?.IsEditable(this) ?? true))
|
||||
bool conditionAllowsEditing = true;
|
||||
if (property.GetAttribute<ConditionallyEditable>() is { } condition)
|
||||
{
|
||||
conditionAllowsEditing = condition.IsEditable(this);
|
||||
}
|
||||
|
||||
bool canAccess = false;
|
||||
if (Container?.GetComponent<CircuitBox>() != null &&
|
||||
Container.CanClientAccess(sender))
|
||||
{
|
||||
//items inside circuit boxes are inaccessible by "normal" means,
|
||||
//but the properties can still be edited through the circuit box UI
|
||||
canAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
canAccess = CanClientAccess(sender);
|
||||
}
|
||||
|
||||
if (!canAccess || !conditionAllowsEditing)
|
||||
{
|
||||
allowEditing = false;
|
||||
}
|
||||
@@ -3799,6 +3831,11 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "itemstats":
|
||||
{
|
||||
item.StatManager.Load(subElement);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
ItemComponent component = unloadedComponents.Find(x => x.Name == subElement.Name.ToString());
|
||||
@@ -3924,7 +3961,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (ItemComponent component in item.components)
|
||||
{
|
||||
if (component.Parent != null) { component.IsActive = component.Parent.IsActive; }
|
||||
if (component.Parent != null && component.InheritParentIsActive) { component.IsActive = component.Parent.IsActive; }
|
||||
component.OnItemLoaded();
|
||||
}
|
||||
|
||||
@@ -3994,6 +4031,8 @@ namespace Barotrauma
|
||||
upgrade.Save(element);
|
||||
}
|
||||
|
||||
statManager?.Save(element);
|
||||
|
||||
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
|
||||
|
||||
var conditionAttribute = element.GetAttribute("condition");
|
||||
|
||||
@@ -23,9 +23,10 @@ namespace Barotrauma
|
||||
Upgrade = 8,
|
||||
ItemStat = 9,
|
||||
DroppedStack = 10,
|
||||
SetHighlight = 11,
|
||||
|
||||
MinValue = 0,
|
||||
MaxValue = 10
|
||||
MaxValue = 11
|
||||
}
|
||||
|
||||
public interface IEventData : NetEntityEvent.IData
|
||||
|
||||
@@ -871,24 +871,43 @@ namespace Barotrauma
|
||||
|
||||
public int GetMaxStackSize(Inventory inventory)
|
||||
{
|
||||
int extraStackSize = inventory switch
|
||||
{
|
||||
ItemInventory { Owner: Item it } i => (int)it.StatManager.GetAdjustedValueAdditive(ItemTalentStats.ExtraStackSize, i.ExtraStackSize),
|
||||
CharacterInventory { Owner: Character { Info: { } info } } i => i.ExtraStackSize + (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, Category.ToIdentifier()),
|
||||
not null => inventory.ExtraStackSize,
|
||||
null => 0
|
||||
};
|
||||
|
||||
if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0)
|
||||
{
|
||||
return maxStackSizeCharacterInventory;
|
||||
return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
|
||||
}
|
||||
else if (inventory?.Owner is Item item &&
|
||||
(item.GetComponent<Holdable>() is { Attachable: false } || item.GetComponent<Wearable>() != null))
|
||||
{
|
||||
if (maxStackSizeHoldableOrWearableInventory > 0)
|
||||
{
|
||||
return maxStackSizeHoldableOrWearableInventory;
|
||||
return MaxStackWithExtra(maxStackSizeHoldableOrWearableInventory, extraStackSize);
|
||||
}
|
||||
else if (maxStackSizeCharacterInventory > 0)
|
||||
{
|
||||
//if maxStackSizeHoldableOrWearableInventory is not set, it defaults to maxStackSizeCharacterInventory
|
||||
return maxStackSizeCharacterInventory;
|
||||
return MaxStackWithExtra(maxStackSizeCharacterInventory, extraStackSize);
|
||||
}
|
||||
}
|
||||
return maxStackSize;
|
||||
|
||||
return MaxStackWithExtra(maxStackSize, extraStackSize);
|
||||
|
||||
static int MaxStackWithExtra(int maxStackSize, int extraStackSize)
|
||||
{
|
||||
extraStackSize = Math.Max(extraStackSize, 0);
|
||||
if (maxStackSize == 1)
|
||||
{
|
||||
return Math.Min(maxStackSize, Inventory.MaxPossibleStackSize);
|
||||
}
|
||||
return Math.Min(maxStackSize + extraStackSize, Inventory.MaxPossibleStackSize);
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
@@ -1138,10 +1157,13 @@ namespace Barotrauma
|
||||
if (fabricationRecipes.TryGetValue(newRecipe.RecipeHash, out var prevRecipe))
|
||||
{
|
||||
//the errors below may be caused by a mod overriding a base item instead of this one, log the package of the base item in that case
|
||||
var packageToLog = GetParentModPackageOrThisPackage();
|
||||
var packageToLog =
|
||||
(variantOf.ContentPackage != null && variantOf.ContentPackage != ContentPackageManager.VanillaCorePackage) ?
|
||||
variantOf.ContentPackage :
|
||||
GetParentModPackageOrThisPackage();
|
||||
|
||||
int prevRecipeIndex = loadedRecipes.IndexOf(prevRecipe);
|
||||
DebugConsole.ThrowError(
|
||||
DebugConsole.AddWarning(
|
||||
$"Error in item prefab \"{ToString()}\": " +
|
||||
$"Fabrication recipe #{loadedRecipes.Count + 1} has the same hash as recipe #{prevRecipeIndex + 1}. This is most likely caused by identical, duplicate recipes. " +
|
||||
$"This will cause issues with fabrication.",
|
||||
|
||||
@@ -2,24 +2,46 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
[NetworkSerialize]
|
||||
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId) : INetSerializableStruct
|
||||
internal readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, Option<UInt32> UniqueCharacterId, bool Save) : INetSerializableStruct
|
||||
{
|
||||
/// <summary>
|
||||
/// Stackable identifiers feature a unique ID to allow multiple stats applied by the same talent from different characters to coexist.
|
||||
/// </summary>
|
||||
public static TalentStatIdentifier CreateStackable(ItemTalentStats stat, Identifier talentIdentifier, UInt32 characterId)
|
||||
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId));
|
||||
=> new(stat, talentIdentifier, Option<UInt32>.Some(characterId), Save: false);
|
||||
|
||||
/// <summary>
|
||||
/// Unstackable identifiers do not have a unique ID causing them to be identical to other stats applied by the same talent from different characters and thus only one of them will be applied.
|
||||
/// <see cref="ItemStatManager.ApplyStat"/> will always use the highest value for unstackable stats.
|
||||
/// </summary>
|
||||
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier)
|
||||
=> new(stat, talentIdentifier, Option.None);
|
||||
public static TalentStatIdentifier CreateUnstackable(ItemTalentStats stat, Identifier talentIdentifier, bool Save)
|
||||
=> new(stat, talentIdentifier, Option.None, Save);
|
||||
|
||||
public XElement Serialize()
|
||||
=> new XElement("Stat",
|
||||
new XAttribute("type", Stat),
|
||||
new XAttribute("talent", TalentIdentifier));
|
||||
|
||||
public static Option<TalentStatIdentifier> TryLoadFromXML(XElement element)
|
||||
{
|
||||
var stat = element.GetAttributeEnum("type", ItemTalentStats.None);
|
||||
var talentIdentifier = element.GetAttributeIdentifier("talent", Identifier.Empty);
|
||||
|
||||
if (stat == ItemTalentStats.None || talentIdentifier == Identifier.Empty)
|
||||
{
|
||||
var error = $"Failed to load talent stat identifier from XML {element}";
|
||||
DebugConsole.ThrowError(error);
|
||||
GameAnalyticsManager.AddErrorEventOnce("ItemStatManager.TryLoadFromXML:Invalid", GameAnalyticsManager.ErrorSeverity.Error, error);
|
||||
return Option.None;
|
||||
}
|
||||
|
||||
return Option.Some(CreateUnstackable(stat, talentIdentifier, true));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ItemStatManager
|
||||
@@ -29,14 +51,14 @@ namespace Barotrauma
|
||||
|
||||
public ItemStatManager(Item item) => this.item = item;
|
||||
|
||||
public void ApplyStat(ItemTalentStats stat, bool stackable, float value, CharacterTalent talent)
|
||||
public void ApplyStat(ItemTalentStats stat, bool stackable, bool save, float value, CharacterTalent talent)
|
||||
{
|
||||
if (talent.Character?.ID is not { } characterId ||
|
||||
talent.Prefab?.Identifier is not { } talentIdentifier) { return; }
|
||||
|
||||
var identifier = stackable
|
||||
? TalentStatIdentifier.CreateStackable(stat, talentIdentifier, characterId)
|
||||
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier);
|
||||
: TalentStatIdentifier.CreateUnstackable(stat, talentIdentifier, save);
|
||||
|
||||
if (!stackable)
|
||||
{
|
||||
@@ -57,12 +79,45 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Save(XElement parent)
|
||||
{
|
||||
var element = new XElement("itemstats");
|
||||
|
||||
foreach (var (key, value) in talentStats)
|
||||
{
|
||||
if (!key.Save) { continue; }
|
||||
|
||||
var statElement = key.Serialize();
|
||||
statElement.Add(new XAttribute("value", value));
|
||||
|
||||
element.Add(statElement);
|
||||
}
|
||||
|
||||
parent.Add(element);
|
||||
}
|
||||
|
||||
public void Load(XElement element)
|
||||
{
|
||||
foreach (XElement statElement in element.Elements())
|
||||
{
|
||||
if (!TalentStatIdentifier.TryLoadFromXML(statElement).TryUnwrap(out var identifier)) { continue; }
|
||||
|
||||
var value = statElement.GetAttributeFloat("value", 0f);
|
||||
|
||||
ApplyStatDirect(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for setting the value value from network packet; bypassing all validity checks.
|
||||
/// </summary>
|
||||
public void ApplyStatDirect(TalentStatIdentifier identifier, float value) => talentStats[identifier] = value;
|
||||
public void ApplyStatDirect(TalentStatIdentifier identifier, float value)
|
||||
=> talentStats[identifier] = value;
|
||||
|
||||
public float GetAdjustedValue(ItemTalentStats stat, float originalValue)
|
||||
/// <summary>
|
||||
/// Adjusts the value by multiplying it with the value of the talent stat
|
||||
/// </summary>
|
||||
public float GetAdjustedValueMultiplicative(ItemTalentStats stat, float originalValue)
|
||||
{
|
||||
float total = originalValue;
|
||||
|
||||
@@ -74,5 +129,21 @@ namespace Barotrauma
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjusts the value by adding the value of the talent stat instead of multiplying it
|
||||
/// </summary>
|
||||
public float GetAdjustedValueAdditive(ItemTalentStats stat, float originalValue)
|
||||
{
|
||||
float total = originalValue;
|
||||
|
||||
foreach (var (key, value) in talentStats)
|
||||
{
|
||||
if (key.Stat != stat) { continue; }
|
||||
total += value;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,6 +442,11 @@ namespace Barotrauma
|
||||
|
||||
public List<DummyFireSource> FakeFireSources { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Can be used by conditionals
|
||||
/// </summary>
|
||||
public int FireCount => FireSources?.Count ?? 0;
|
||||
|
||||
public BallastFloraBehavior BallastFlora { get; set; }
|
||||
|
||||
public Hull(Rectangle rectangle)
|
||||
|
||||
@@ -633,14 +633,14 @@ namespace Barotrauma
|
||||
{
|
||||
endHole = new Tunnel(
|
||||
TunnelType.SidePath,
|
||||
new List<Point>() { startPosition, startExitPosition, new Point(0, Size.Y) },
|
||||
new List<Point>() { startPosition, new Point(0, startPosition.Y) },
|
||||
minWidth, parentTunnel: mainPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
endHole = new Tunnel(
|
||||
TunnelType.SidePath,
|
||||
new List<Point>() { endPosition, endExitPosition, Size },
|
||||
new List<Point>() { endPosition, new Point(Size.X, endPosition.Y) },
|
||||
minWidth, parentTunnel: mainPath);
|
||||
}
|
||||
Tunnels.Add(endHole);
|
||||
@@ -4122,7 +4122,7 @@ namespace Barotrauma
|
||||
|
||||
if (location != null)
|
||||
{
|
||||
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.Name}, level type: {LevelData.Type})");
|
||||
DebugConsole.NewMessage($"Generating an outpost for the {(isStart ? "start" : "end")} of the level... (Location: {location.DisplayName}, level type: {LevelData.Type})");
|
||||
outpost = OutpostGenerator.Generate(outpostGenerationParams, location, onlyEntrance: LevelData.Type != LevelData.LevelType.Outpost, LevelData.AllowInvalidOutpost);
|
||||
}
|
||||
else
|
||||
@@ -4230,7 +4230,20 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
spawnPos = outpost.FindSpawnPos(i == 0 ? StartPosition : EndPosition, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
|
||||
Vector2 preferredSpawnPos = i == 0 ? StartPosition : EndPosition;
|
||||
//if we're placing the outpost at the end of the level, close to the bottom-right,
|
||||
//and there's a hole leading out the right side of the level, move the spawn position towards that hole.
|
||||
//Makes outpost placement a little nicer in levels with lots of verticality: if there's a tall vertical
|
||||
//shaft leading down to the end position, we don't want the outpost to be placed all the way up to wherever the
|
||||
//ceiling is at the top of that shaft.
|
||||
if (i == 1 && GenerationParams.CreateHoleNextToEnd &&
|
||||
preferredSpawnPos.X > Size.X * 0.75f &&
|
||||
preferredSpawnPos.Y < Size.Y * 0.25f)
|
||||
{
|
||||
preferredSpawnPos.X = (preferredSpawnPos.X + Size.X) / 2;
|
||||
}
|
||||
|
||||
spawnPos = outpost.FindSpawnPos(preferredSpawnPos, minSize, outpostDockingPortOffset != null ? subDockingPortOffset - outpostDockingPortOffset.Value : 0.0f, verticalMoveDir: 1);
|
||||
if (Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
spawnPos.Y = Math.Min(Size.Y - outpost.Borders.Height * 0.6f, spawnPos.Y + outpost.Borders.Height / 2);
|
||||
@@ -4254,7 +4267,7 @@ namespace Barotrauma
|
||||
if (StartLocation != null)
|
||||
{
|
||||
outpost.TeamID = StartLocation.Type.OutpostTeam;
|
||||
outpost.Info.Name = StartLocation.Name;
|
||||
outpost.Info.Name = StartLocation.DisplayName.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -4263,7 +4276,7 @@ namespace Barotrauma
|
||||
if (EndLocation != null)
|
||||
{
|
||||
outpost.TeamID = EndLocation.Type.OutpostTeam;
|
||||
outpost.Info.Name = EndLocation.Name;
|
||||
outpost.Info.Name = EndLocation.DisplayName.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public LevelData(Location location, Map map, float difficulty)
|
||||
{
|
||||
Seed = location.BaseName + map.Locations.IndexOf(location);
|
||||
Seed = location.NameIdentifier.Value + map.Locations.IndexOf(location);
|
||||
Biome = location.Biome;
|
||||
Type = LevelType.Outpost;
|
||||
Difficulty = difficulty;
|
||||
|
||||
@@ -639,6 +639,7 @@ namespace Barotrauma
|
||||
|
||||
public override void Remove()
|
||||
{
|
||||
objectsInRange.Clear();
|
||||
if (objects != null)
|
||||
{
|
||||
foreach (LevelObject obj in objects)
|
||||
|
||||
@@ -55,8 +55,17 @@ namespace Barotrauma
|
||||
|
||||
public readonly List<LocationConnection> Connections = new List<LocationConnection>();
|
||||
|
||||
private string baseName;
|
||||
public LocalizedString DisplayName { get; private set; }
|
||||
|
||||
public Identifier NameIdentifier => nameIdentifier;
|
||||
|
||||
private int nameFormatIndex;
|
||||
private Identifier nameIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// For backwards compatibility: a non-localizable name from the old text files.
|
||||
/// </summary>
|
||||
private string rawName;
|
||||
|
||||
private LocationType addInitialMissionsForType;
|
||||
|
||||
@@ -75,10 +84,6 @@ namespace Barotrauma
|
||||
|
||||
public bool DisallowLocationTypeChanges;
|
||||
|
||||
public string BaseName { get => baseName; }
|
||||
|
||||
public string Name { get; private set; }
|
||||
|
||||
public Biome Biome { get; set; }
|
||||
|
||||
public Vector2 MapPosition { get; private set; }
|
||||
@@ -309,7 +314,7 @@ namespace Barotrauma
|
||||
if (!faction.IsEmpty && GameMain.GameSession.Campaign.GetFactionAffiliation(faction) is FactionAffiliation.Positive)
|
||||
{
|
||||
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
|
||||
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, new Identifier("all")));
|
||||
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
|
||||
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
|
||||
}
|
||||
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
|
||||
@@ -484,7 +489,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (missionIndex < 0 || missionIndex >= availableMissions.Count)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to select a mission in location \"{Name}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
|
||||
DebugConsole.ThrowError($"Failed to select a mission in location \"{DisplayName}\". Mission index out of bounds ({missionIndex}, available missions: {availableMissions.Count})");
|
||||
break;
|
||||
}
|
||||
selectedMissions.Add(availableMissions[missionIndex]);
|
||||
@@ -536,15 +541,15 @@ namespace Barotrauma
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Location ({Name ?? "null"})";
|
||||
return $"Location ({DisplayName ?? "null"})";
|
||||
}
|
||||
|
||||
public Location(Vector2 mapPosition, int? zone, Random rand, bool requireOutpost = false, LocationType forceLocationType = null, IEnumerable<Location> existingLocations = null)
|
||||
{
|
||||
Type = OriginalType = forceLocationType ?? LocationType.Random(rand, zone, requireOutpost);
|
||||
Name = RandomName(Type, rand, existingLocations);
|
||||
CreateRandomName(Type, rand, existingLocations);
|
||||
MapPosition = mapPosition;
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
PortraitId = ToolBox.StringToInt(nameIdentifier.Value);
|
||||
Connections = new List<LocationConnection>();
|
||||
}
|
||||
|
||||
@@ -561,9 +566,21 @@ namespace Barotrauma
|
||||
GetTypeOrFallback(originalLocationTypeId, out LocationType originalType);
|
||||
OriginalType = originalType;
|
||||
|
||||
baseName = element.GetAttributeString("basename", "");
|
||||
Name = element.GetAttributeString("name", "");
|
||||
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
|
||||
nameIdentifier = element.GetAttributeIdentifier(nameof(nameIdentifier), "");
|
||||
if (nameIdentifier.IsEmpty)
|
||||
{
|
||||
//backwards compatibility
|
||||
rawName = element.GetAttributeString("basename", "");
|
||||
nameIdentifier = rawName.ToIdentifier();
|
||||
DisplayName = element.GetAttributeString("name", "");
|
||||
}
|
||||
else
|
||||
{
|
||||
nameFormatIndex = element.GetAttributeInt(nameof(nameFormatIndex), 0);
|
||||
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
|
||||
}
|
||||
|
||||
MapPosition = element.GetAttributeVector2("position", Vector2.Zero);
|
||||
|
||||
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 1.0f);
|
||||
IsGateBetweenBiomes = element.GetAttributeBool("isgatebetweenbiomes", false);
|
||||
@@ -641,7 +658,7 @@ namespace Barotrauma
|
||||
|
||||
LevelData = new LevelData(element.GetChildElement("Level"), clampDifficultyToBiome: true);
|
||||
|
||||
PortraitId = ToolBox.StringToInt(Name);
|
||||
PortraitId = ToolBox.StringToInt(!rawName.IsNullOrEmpty() ? rawName : nameIdentifier.Value);
|
||||
|
||||
LoadStores(element);
|
||||
LoadMissions(element);
|
||||
@@ -687,7 +704,7 @@ namespace Barotrauma
|
||||
int locationTypeChangeIndex = subElement.GetAttributeInt("index", 0);
|
||||
if (locationTypeChangeIndex < 0 || locationTypeChangeIndex >= Type.CanChangeTo.Count)
|
||||
{
|
||||
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{Name}\". Location index out of bounds ({locationTypeChangeIndex}).");
|
||||
DebugConsole.AddWarning($"Failed to activate a location type change in the location \"{DisplayName}\". Location index out of bounds ({locationTypeChangeIndex}).");
|
||||
continue;
|
||||
}
|
||||
PendingLocationTypeChange = (Type.CanChangeTo[locationTypeChangeIndex], timer, null);
|
||||
@@ -698,7 +715,7 @@ namespace Barotrauma
|
||||
var mission = MissionPrefab.Prefabs[missionIdentifier];
|
||||
if (mission == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{Name}\". Matching mission not found.");
|
||||
DebugConsole.AddWarning($"Failed to activate a location type change from the mission \"{missionIdentifier}\" in location \"{DisplayName}\". Matching mission not found.");
|
||||
continue;
|
||||
}
|
||||
PendingLocationTypeChange = (mission.LocationTypeChangeOnCompleted, timer, mission);
|
||||
@@ -735,14 +752,27 @@ namespace Barotrauma
|
||||
|
||||
if (newType == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to change the type of the location \"{Name}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
DebugConsole.ThrowError($"Failed to change the type of the location \"{DisplayName}\" to null.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
}
|
||||
|
||||
DebugConsole.Log("Location " + baseName + " changed it's type from " + Type + " to " + newType);
|
||||
|
||||
Type = newType;
|
||||
Name = Type.NameFormats == null || !Type.NameFormats.Any() ? baseName : Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", baseName);
|
||||
if (rawName != null)
|
||||
{
|
||||
DebugConsole.Log($"Location {rawName} changed it's type from {Type} to {newType}");
|
||||
DisplayName =
|
||||
Type.NameFormats == null || !Type.NameFormats.Any() ?
|
||||
rawName :
|
||||
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", rawName);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.Log($"Location {DisplayName.Value} changed it's type from {Type} to {newType}");
|
||||
DisplayName =
|
||||
Type.NameFormats == null || !Type.NameFormats.Any() ?
|
||||
TextManager.Get(nameIdentifier) :
|
||||
Type.NameFormats[nameFormatIndex % Type.NameFormats.Count].Replace("[name]", TextManager.Get(nameIdentifier).Value);
|
||||
}
|
||||
|
||||
if (Type.HasOutpost && Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
@@ -1058,12 +1088,12 @@ namespace Barotrauma
|
||||
{
|
||||
if (!Type.HasHireableCharacters)
|
||||
{
|
||||
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - the location has no hireable characters.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
}
|
||||
if (HireManager == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Cannot hire a character from location \"" + Name + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
DebugConsole.ThrowError("Cannot hire a character from location \"" + DisplayName + "\" - hire manager has not been instantiated.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1086,22 +1116,52 @@ namespace Barotrauma
|
||||
return HireManager.AvailableCharacters;
|
||||
}
|
||||
|
||||
private string RandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
|
||||
private void CreateRandomName(LocationType type, Random rand, IEnumerable<Location> existingLocations)
|
||||
{
|
||||
if (!type.ForceLocationName.IsNullOrEmpty())
|
||||
if (!type.ForceLocationName.IsEmpty)
|
||||
{
|
||||
baseName = type.ForceLocationName.Value;
|
||||
return baseName;
|
||||
nameIdentifier = type.ForceLocationName;
|
||||
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
|
||||
return;
|
||||
}
|
||||
nameIdentifier = type.GetRandomNameId(rand, existingLocations);
|
||||
if (nameIdentifier.IsEmpty)
|
||||
{
|
||||
rawName = type.GetRandomRawName(rand, existingLocations);
|
||||
if (rawName.IsNullOrEmpty())
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to generate a name for a location of the type {type.Identifier}. No names found in localization files or the .txt files.");
|
||||
rawName = "none";
|
||||
}
|
||||
nameIdentifier = rawName.ToIdentifier();
|
||||
DisplayName = rawName;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (type.NameFormats == null || !type.NameFormats.Any())
|
||||
{
|
||||
DisplayName = TextManager.Get(nameIdentifier).Fallback(nameIdentifier.Value);
|
||||
return;
|
||||
}
|
||||
nameFormatIndex = rand.Next() % type.NameFormats.Count;
|
||||
DisplayName = GetName(Type, nameFormatIndex, nameIdentifier);
|
||||
}
|
||||
baseName = type.GetRandomName(rand, existingLocations);
|
||||
if (type.NameFormats == null || !type.NameFormats.Any()) { return baseName; }
|
||||
nameFormatIndex = rand.Next() % type.NameFormats.Count;
|
||||
return type.NameFormats[nameFormatIndex].Replace("[name]", baseName);
|
||||
}
|
||||
|
||||
public void ForceName(string name)
|
||||
private static LocalizedString GetName(LocationType type, int nameFormatIndex, Identifier nameId)
|
||||
{
|
||||
baseName = Name = name;
|
||||
if (type?.NameFormats == null || !type.NameFormats.Any())
|
||||
{
|
||||
return TextManager.Get(nameId);
|
||||
}
|
||||
return type.NameFormats[nameFormatIndex % type.NameFormats.Count].Replace("[name]", TextManager.Get(nameId).Value);
|
||||
}
|
||||
|
||||
public void ForceName(Identifier nameId)
|
||||
{
|
||||
rawName = string.Empty;
|
||||
nameIdentifier = nameId;
|
||||
DisplayName = TextManager.Get(nameId).Fallback(nameId.Value);
|
||||
}
|
||||
|
||||
public void LoadStores(XElement locationElement)
|
||||
@@ -1125,7 +1185,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": duplicate identifier.";
|
||||
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": duplicate identifier.";
|
||||
DebugConsole.ThrowError(msg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:DuplicateStoreInfo", GameAnalyticsManager.ErrorSeverity.Error, msg);
|
||||
continue;
|
||||
@@ -1133,7 +1193,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
string msg = $"Error loading store info for \"{identifier}\" at location {Name} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
|
||||
string msg = $"Error loading store info for \"{identifier}\" at location {DisplayName} of type \"{Type.Identifier}\": location shouldn't contain a store with this identifier.";
|
||||
DebugConsole.ThrowError(msg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Location.LoadStore:IncorrectStoreIdentifier", GameAnalyticsManager.ErrorSeverity.Error, msg);
|
||||
continue;
|
||||
@@ -1444,8 +1504,9 @@ namespace Barotrauma
|
||||
var locationElement = new XElement("location",
|
||||
new XAttribute("type", Type.Identifier),
|
||||
new XAttribute("originaltype", (Type ?? OriginalType).Identifier),
|
||||
new XAttribute("basename", BaseName),
|
||||
new XAttribute("name", Name),
|
||||
/*not used currently (we load the nameIdentifier instead),
|
||||
* but could make sense to include still for backwards compatibility reasons*/
|
||||
new XAttribute("name", DisplayName),
|
||||
new XAttribute("biome", Biome?.Identifier.Value ?? string.Empty),
|
||||
new XAttribute("position", XMLExtensions.Vector2ToString(MapPosition)),
|
||||
new XAttribute("pricemultiplier", PriceMultiplier),
|
||||
@@ -1455,6 +1516,16 @@ namespace Barotrauma
|
||||
new XAttribute(nameof(TurnsInRadiation).ToLower(), TurnsInRadiation),
|
||||
new XAttribute("stepssincespecialsupdated", StepsSinceSpecialsUpdated));
|
||||
|
||||
if (!rawName.IsNullOrEmpty())
|
||||
{
|
||||
locationElement.Add(new XAttribute(nameof(rawName), rawName));
|
||||
}
|
||||
else
|
||||
{
|
||||
locationElement.Add(new XAttribute(nameof(nameIdentifier), nameIdentifier));
|
||||
locationElement.Add(new XAttribute(nameof(nameFormatIndex), nameFormatIndex));
|
||||
}
|
||||
|
||||
if (Faction != null)
|
||||
{
|
||||
locationElement.Add(new XAttribute("faction", Faction.Prefab.Identifier));
|
||||
@@ -1491,7 +1562,7 @@ namespace Barotrauma
|
||||
changeElement.Add(new XAttribute("index", index));
|
||||
if (index == -1)
|
||||
{
|
||||
DebugConsole.AddWarning($"Invalid location type change in the location \"{Name}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
|
||||
DebugConsole.AddWarning($"Invalid location type change in the location \"{DisplayName}\". Unknown type change ({PendingLocationTypeChange.Value.typeChange.ChangeToType}).");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -13,7 +14,7 @@ namespace Barotrauma
|
||||
{
|
||||
public static readonly PrefabCollection<LocationType> Prefabs = new PrefabCollection<LocationType>();
|
||||
|
||||
private readonly ImmutableArray<string> names;
|
||||
private readonly ImmutableArray<string> rawNames;
|
||||
private readonly ImmutableArray<Sprite> portraits;
|
||||
|
||||
//<name, commonness>
|
||||
@@ -26,7 +27,7 @@ namespace Barotrauma
|
||||
public readonly LocalizedString Name;
|
||||
public readonly LocalizedString Description;
|
||||
|
||||
public readonly LocalizedString ForceLocationName;
|
||||
public readonly Identifier ForceLocationName;
|
||||
|
||||
public readonly float BeaconStationChance;
|
||||
|
||||
@@ -54,12 +55,20 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
private readonly ImmutableArray<Identifier>? nameIdentifiers = null;
|
||||
|
||||
private LanguageIdentifier nameFormatLanguage;
|
||||
|
||||
private ImmutableArray<string>? nameFormats = null;
|
||||
public IReadOnlyList<string> NameFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
nameFormats ??= TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray();
|
||||
if (nameFormats == null || GameSettings.CurrentConfig.Language != nameFormatLanguage)
|
||||
{
|
||||
nameFormats = TextManager.GetAll($"LocationNameFormat.{Identifier}").ToImmutableArray();
|
||||
nameFormatLanguage = GameSettings.CurrentConfig.Language;
|
||||
}
|
||||
return nameFormats;
|
||||
}
|
||||
}
|
||||
@@ -143,29 +152,37 @@ namespace Barotrauma
|
||||
|
||||
if (element.GetAttribute("name") != null)
|
||||
{
|
||||
ForceLocationName = TextManager.Get(element.GetAttributeString("name", string.Empty));
|
||||
ForceLocationName = element.GetAttributeIdentifier("name", string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
string[] rawNamePaths = element.GetAttributeStringArray("namefile", new string[] { "Content/Map/locationNames.txt" });
|
||||
var names = new List<string>();
|
||||
foreach (string rawPath in rawNamePaths)
|
||||
//backwards compatibility for location names defined in a text file
|
||||
string[] rawNamePaths = element.GetAttributeStringArray("namefile", Array.Empty<string>());
|
||||
if (rawNamePaths.Any())
|
||||
{
|
||||
try
|
||||
foreach (string rawPath in rawNamePaths)
|
||||
{
|
||||
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
|
||||
names.AddRange(File.ReadAllLines(path.Value).ToList());
|
||||
try
|
||||
{
|
||||
var path = ContentPath.FromRaw(element.ContentPackage, rawPath.Trim());
|
||||
names.AddRange(File.ReadAllLines(path.Value).ToList());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
if (!names.Any())
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to read name file \"rawPath\" for location type \"{Identifier}\"!", e);
|
||||
names.Add("ERROR: No names found");
|
||||
}
|
||||
this.rawNames = names.ToImmutableArray();
|
||||
}
|
||||
if (!names.Any())
|
||||
else
|
||||
{
|
||||
names.Add("ERROR: No names found");
|
||||
nameIdentifiers = element.GetAttributeIdentifierArray("nameidentifiers", new Identifier[] { Identifier }).ToImmutableArray();
|
||||
}
|
||||
this.names = names.ToImmutableArray();
|
||||
}
|
||||
|
||||
string[] commonnessPerZoneStrs = element.GetAttributeStringArray("commonnessperzone", Array.Empty<string>());
|
||||
@@ -259,17 +276,64 @@ namespace Barotrauma
|
||||
return portraits[Math.Abs(randomSeed) % portraits.Length];
|
||||
}
|
||||
|
||||
public string GetRandomName(Random rand, IEnumerable<Location> existingLocations)
|
||||
public Identifier GetRandomNameId(Random rand, IEnumerable<Location> existingLocations)
|
||||
{
|
||||
if (nameIdentifiers == null)
|
||||
{
|
||||
return Identifier.Empty;
|
||||
}
|
||||
List<Identifier> nameIds = new List<Identifier>();
|
||||
foreach (var nameId in nameIdentifiers)
|
||||
{
|
||||
int index = 0;
|
||||
while (true)
|
||||
{
|
||||
Identifier tag = $"LocationName.{nameId}.{index}".ToIdentifier();
|
||||
if (TextManager.ContainsTag(tag, TextManager.DefaultLanguage))
|
||||
{
|
||||
nameIds.Add(tag);
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
DebugConsole.ThrowError($"Could not find any location names for the location type {Identifier}. Name identifier: {nameId}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nameIds.None())
|
||||
{
|
||||
return Identifier.Empty;
|
||||
}
|
||||
if (existingLocations != null)
|
||||
{
|
||||
var unusedNames = names.Where(name => !existingLocations.Any(l => l.BaseName == name)).ToList();
|
||||
var unusedNameIds = nameIds.FindAll(nameId => existingLocations.None(l => l.NameIdentifier == nameId));
|
||||
if (unusedNameIds.Count > 0)
|
||||
{
|
||||
return unusedNameIds[rand.Next() % unusedNameIds.Count];
|
||||
}
|
||||
}
|
||||
return nameIds[rand.Next() % nameIds.Count];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For backwards compatibility. Chooses a random name from the names defined in the .txt name files (<see cref="rawNamePaths"/>).
|
||||
/// </summary>
|
||||
public string GetRandomRawName(Random rand, IEnumerable<Location> existingLocations)
|
||||
{
|
||||
if (rawNames == null || rawNames.None()) { return string.Empty; }
|
||||
if (existingLocations != null)
|
||||
{
|
||||
var unusedNames = rawNames.Where(name => !existingLocations.Any(l => l.DisplayName.Value == name)).ToList();
|
||||
if (unusedNames.Count > 0)
|
||||
{
|
||||
return unusedNames[rand.Next() % unusedNames.Count];
|
||||
}
|
||||
}
|
||||
return names[rand.Next() % names.Length];
|
||||
return rawNames[rand.Next() % rawNames.Length];
|
||||
}
|
||||
|
||||
public static LocationType Random(Random rand, int? zone = null, bool requireOutpost = false, Func<LocationType, bool> predicate = null)
|
||||
|
||||
@@ -262,9 +262,9 @@ namespace Barotrauma
|
||||
foreach (var endLocation in EndLocations)
|
||||
{
|
||||
if (endLocation.Type?.ForceLocationName != null &&
|
||||
!endLocation.Type.ForceLocationName.IsNullOrEmpty())
|
||||
!endLocation.Type.ForceLocationName.IsEmpty)
|
||||
{
|
||||
endLocation.ForceName(endLocation.Type.ForceLocationName.Value);
|
||||
endLocation.ForceName(endLocation.Type.ForceLocationName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1005,10 +1005,10 @@ namespace Barotrauma
|
||||
CurrentLocation.CreateStores();
|
||||
OnLocationChanged?.Invoke(new LocationChangeInfo(prevLocation, CurrentLocation));
|
||||
|
||||
if (GameMain.GameSession is { Campaign: { CampaignMetadata: { } metadata } })
|
||||
if (GameMain.GameSession is { Campaign.CampaignMetadata: { } metadata })
|
||||
{
|
||||
metadata.SetValue("campaign.location.id".ToIdentifier(), CurrentLocationIndex);
|
||||
metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.Name);
|
||||
metadata.SetValue("campaign.location.name".ToIdentifier(), CurrentLocation.NameIdentifier.Value);
|
||||
metadata.SetValue("campaign.location.biome".ToIdentifier(), CurrentLocation.Biome?.Identifier ?? "null".ToIdentifier());
|
||||
metadata.SetValue("campaign.location.type".ToIdentifier(), CurrentLocation.Type?.Identifier ?? "null".ToIdentifier());
|
||||
}
|
||||
@@ -1077,7 +1077,7 @@ namespace Barotrauma
|
||||
if (SelectedConnection?.Locked ?? false)
|
||||
{
|
||||
string errorMsg =
|
||||
$"A locked connection was selected ({SelectedConnection.Locations[0].Name} -> {SelectedConnection.Locations[1].Name}." +
|
||||
$"A locked connection was selected ({SelectedConnection.Locations[0].DisplayName} -> {SelectedConnection.Locations[1].DisplayName}." +
|
||||
$" Current location: {CurrentLocation}, current display location: {currentDisplayLocation}).\n"
|
||||
+ Environment.StackTrace.CleanupStackTrace();
|
||||
GameAnalyticsManager.AddErrorEventOnce("MapSelectLocation:LockedConnectionSelected", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
@@ -1093,7 +1093,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!Locations.Contains(location))
|
||||
{
|
||||
string errorMsg = "Failed to select a location. " + (location?.Name ?? "null") + " not found in the map.";
|
||||
string errorMsg = $"Failed to select a location. {location?.DisplayName ?? "null"} not found in the map.";
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Map.SelectLocation:LocationNotFound", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
return;
|
||||
@@ -1301,11 +1301,11 @@ namespace Barotrauma
|
||||
|
||||
private bool ChangeLocationType(CampaignMode campaign, Location location, LocationTypeChange change)
|
||||
{
|
||||
string prevName = location.Name;
|
||||
LocalizedString prevName = location.DisplayName;
|
||||
|
||||
if (!LocationType.Prefabs.TryGet(change.ChangeToType, out var newType))
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.Name}\". Location type \"{change.ChangeToType}\" not found.");
|
||||
DebugConsole.ThrowError($"Failed to change the type of the location \"{location.DisplayName}\". Location type \"{change.ChangeToType}\" not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1372,7 +1372,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
partial void ChangeLocationTypeProjSpecific(Location location, string prevName, LocationTypeChange change);
|
||||
partial void ChangeLocationTypeProjSpecific(Location location, LocalizedString prevName, LocationTypeChange change);
|
||||
|
||||
partial void ClearAnimQueue();
|
||||
|
||||
@@ -1498,7 +1498,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Identifier locationType = subElement.GetAttributeIdentifier("type", Identifier.Empty);
|
||||
string prevLocationName = location.Name;
|
||||
LocalizedString prevLocationName = location.DisplayName;
|
||||
LocationType prevLocationType = location.Type;
|
||||
LocationType newLocationType = LocationType.Prefabs.Find(lt => lt.Identifier == locationType) ?? LocationType.Prefabs.First();
|
||||
location.ChangeType(campaign, newLocationType);
|
||||
@@ -1619,7 +1619,7 @@ namespace Barotrauma
|
||||
//this should not be possible, you can't enter non-outpost locations (= natural formations)
|
||||
if (CurrentLocation != null && !CurrentLocation.Type.HasOutpost && SelectedConnection == null)
|
||||
{
|
||||
DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.Name}). Loading the first adjacent connection...");
|
||||
DebugConsole.AddWarning($"Error while loading campaign map state. Submarine in a location with no outpost ({CurrentLocation.DisplayName}). Loading the first adjacent connection...");
|
||||
SelectLocation(CurrentLocation.Connections[0].OtherLocation(CurrentLocation));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,15 +576,10 @@ namespace Barotrauma
|
||||
base.Remove();
|
||||
|
||||
MapEntityList.Remove(this);
|
||||
|
||||
#if CLIENT
|
||||
Submarine.ForceRemoveFromVisibleEntities(this);
|
||||
if (SelectedList.Contains(this))
|
||||
{
|
||||
SelectedList = SelectedList.Where(e => e != this).ToHashSet();
|
||||
}
|
||||
SelectedList.Remove(this);
|
||||
#endif
|
||||
|
||||
if (aiTarget != null)
|
||||
{
|
||||
aiTarget.Remove();
|
||||
@@ -686,6 +681,9 @@ namespace Barotrauma
|
||||
Move(-relative * 2.0f);
|
||||
}
|
||||
|
||||
public virtual Quad2D GetTransformedQuad()
|
||||
=> Quad2D.FromSubmarineRectangle(rect);
|
||||
|
||||
public static List<MapEntity> LoadAll(Submarine submarine, XElement parentElement, string filePath, int idOffset)
|
||||
{
|
||||
IdRemap idRemap = new IdRemap(parentElement, idOffset);
|
||||
|
||||
@@ -733,6 +733,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override Quad2D GetTransformedQuad()
|
||||
=> Quad2D.FromSubmarineRectangle(rect).Rotated(
|
||||
FlippedX != FlippedY
|
||||
? rotationRad
|
||||
: -rotationRad);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there's a structure items can be attached to at the given position and returns it.
|
||||
/// </summary>
|
||||
@@ -912,6 +918,12 @@ namespace Barotrauma
|
||||
return Sections[sectionIndex].damage >= MaxHealth * LeakThreshold;
|
||||
}
|
||||
|
||||
public bool SectionIsLeakingFromOutside(int sectionIndex)
|
||||
{
|
||||
if (sectionIndex < 0 || sectionIndex >= Sections.Length) { return false; }
|
||||
return SectionIsLeaking(sectionIndex) && !Sections[sectionIndex].gap.IsRoomToRoom;
|
||||
}
|
||||
|
||||
public int SectionLength(int sectionIndex)
|
||||
{
|
||||
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0;
|
||||
@@ -1304,8 +1316,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (damageDiff < 0.0f)
|
||||
{
|
||||
attacker.Info?.IncreaseSkillLevel("mechanical".ToIdentifier(),
|
||||
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f));
|
||||
attacker.Info?.ApplySkillGain(Barotrauma.Tags.MechanicalSkill,
|
||||
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,51 +505,58 @@ namespace Barotrauma
|
||||
minWidth += padding;
|
||||
minHeight += padding;
|
||||
|
||||
Vector2 limits = GetHorizontalLimits(spawnPos, minWidth, minHeight, 0);
|
||||
if (verticalMoveDir != 0)
|
||||
int iterations = 0;
|
||||
const int maxIterations = 5;
|
||||
do
|
||||
{
|
||||
verticalMoveDir = Math.Sign(verticalMoveDir);
|
||||
//do a raycast towards the top/bottom of the level depending on direction
|
||||
Vector2 potentialPos = new Vector2(spawnPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
|
||||
|
||||
//3 raycasts (left, middle and right side of the sub, so we don't accidentally raycast up a passage too narrow for the sub)
|
||||
for (int x = -1; x <= 1; x++)
|
||||
Vector2 potentialPos = spawnPos;
|
||||
if (verticalMoveDir != 0)
|
||||
{
|
||||
Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x;
|
||||
if (PickBody(
|
||||
ConvertUnits.ToSimUnits(spawnPos + xOffset),
|
||||
ConvertUnits.ToSimUnits(potentialPos + xOffset),
|
||||
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall) != null)
|
||||
verticalMoveDir = Math.Sign(verticalMoveDir);
|
||||
//do a raycast towards the top/bottom of the level depending on direction
|
||||
Vector2 rayEnd = new Vector2(potentialPos.X, verticalMoveDir > 0 ? Level.Loaded.Size.Y : 0);
|
||||
|
||||
Vector2 closestPickedPos = rayEnd;
|
||||
//multiple raycast across the width of the sub (so we don't accidentally raycast up a passage too narrow for the sub)
|
||||
for (float x = -1; x <= 1; x += 0.2f)
|
||||
{
|
||||
int offsetFromWall = 10 * -verticalMoveDir;
|
||||
//if the raycast hit a wall, attempt to place the spawnpos there
|
||||
if (verticalMoveDir > 0)
|
||||
Vector2 xOffset = Vector2.UnitX * minWidth / 2 * x;
|
||||
xOffset.X += subDockingPortOffset;
|
||||
if (PickBody(
|
||||
ConvertUnits.ToSimUnits(potentialPos + xOffset),
|
||||
ConvertUnits.ToSimUnits(rayEnd + xOffset),
|
||||
collisionCategory: Physics.CollisionLevel | Physics.CollisionWall,
|
||||
customPredicate: (Fixture f) =>
|
||||
{
|
||||
return f.UserData is not VoronoiCell { IsDestructible: true };
|
||||
}) != null)
|
||||
{
|
||||
potentialPos.Y = Math.Min(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall);
|
||||
}
|
||||
else
|
||||
{
|
||||
potentialPos.Y = Math.Max(potentialPos.Y, ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall);
|
||||
//if the raycast hit a wall, attempt to place the spawnpos there
|
||||
int offsetFromWall = 10 * -verticalMoveDir;
|
||||
float pickedPos = ConvertUnits.ToDisplayUnits(LastPickedPosition.Y) + offsetFromWall;
|
||||
closestPickedPos.Y =
|
||||
verticalMoveDir > 0 ?
|
||||
Math.Min(closestPickedPos.Y, pickedPos) :
|
||||
Math.Max(closestPickedPos.Y, pickedPos);
|
||||
}
|
||||
}
|
||||
potentialPos.Y = closestPickedPos.Y;
|
||||
}
|
||||
|
||||
//step away from the top/bottom of the level, or from whatever wall the raycast hit,
|
||||
//until we found a spot where there's enough room to place the sub
|
||||
float dist = Math.Abs(potentialPos.Y - spawnPos.Y);
|
||||
for (float d = dist; d > 0; d -= 100.0f)
|
||||
Vector2 limits = GetHorizontalLimits(new Vector2(potentialPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir)),
|
||||
maxHorizontalMoveAmount: minWidth, minHeight, verticalMoveDir, padding);
|
||||
if (limits.Y - limits.X >= minWidth)
|
||||
{
|
||||
float y = spawnPos.Y + verticalMoveDir * d;
|
||||
limits = GetHorizontalLimits(new Vector2(spawnPos.X, y), minWidth, minHeight, verticalMoveDir);
|
||||
if (limits.Y - limits.X > minWidth)
|
||||
{
|
||||
spawnPos = new Vector2(spawnPos.X, y - (dockedBorders.Height * 0.5f * verticalMoveDir));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vector2 newSpawnPos = new Vector2(spawnPos.X, potentialPos.Y - (dockedBorders.Height * 0.5f * verticalMoveDir));
|
||||
bool couldMoveInVerticalMoveDir = Math.Sign(newSpawnPos.Y - spawnPos.Y) == Math.Sign(verticalMoveDir);
|
||||
if (!couldMoveInVerticalMoveDir) { break; }
|
||||
spawnPos = ClampToHorizontalLimits(newSpawnPos, limits);
|
||||
}
|
||||
|
||||
static Vector2 GetHorizontalLimits(Vector2 spawnPos, float minWidth, float minHeight, int verticalMoveDir)
|
||||
iterations++;
|
||||
} while (iterations < maxIterations);
|
||||
|
||||
Vector2 GetHorizontalLimits(Vector2 spawnPos, float maxHorizontalMoveAmount, float minHeight, int verticalMoveDir, int padding)
|
||||
{
|
||||
Vector2 refPos = spawnPos - Vector2.UnitY * minHeight * 0.5f * Math.Sign(verticalMoveDir);
|
||||
|
||||
@@ -580,34 +587,44 @@ namespace Barotrauma
|
||||
if (Math.Abs(ruin.Area.Center.Y - refPos.Y) > (minHeight + ruin.Area.Height) * 0.5f) { continue; }
|
||||
if (ruin.Area.Center.X < refPos.X)
|
||||
{
|
||||
minX = Math.Max(minX, ruin.Area.Right + 100.0f);
|
||||
minX = Math.Max(minX, ruin.Area.Right + padding);
|
||||
}
|
||||
else
|
||||
{
|
||||
maxX = Math.Min(maxX, ruin.Area.X - 100.0f);
|
||||
maxX = Math.Min(maxX, ruin.Area.X - padding);
|
||||
}
|
||||
}
|
||||
return new Vector2(Math.Max(minX, spawnPos.X - minWidth), Math.Min(maxX, spawnPos.X + minWidth));
|
||||
|
||||
minX += subDockingPortOffset;
|
||||
maxX += subDockingPortOffset;
|
||||
|
||||
return new Vector2(
|
||||
Math.Max(Math.Max(minX, spawnPos.X - maxHorizontalMoveAmount - padding), 0),
|
||||
Math.Min(Math.Min(maxX, spawnPos.X + maxHorizontalMoveAmount + padding), Level.Loaded.Size.X));
|
||||
}
|
||||
|
||||
if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
|
||||
Vector2 ClampToHorizontalLimits(Vector2 spawnPos, Vector2 limits)
|
||||
{
|
||||
//no walls found at either side, just use the initial spawnpos and hope for the best
|
||||
}
|
||||
else if (limits.X < 0)
|
||||
{
|
||||
//no wall found at the left side, spawn to the left from the right-side wall
|
||||
spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
|
||||
}
|
||||
else if (limits.Y > Level.Loaded.Size.X)
|
||||
{
|
||||
//no wall found at right side, spawn to the right from the left-side wall
|
||||
spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
//walls found at both sides, use their midpoint
|
||||
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
|
||||
if (limits.X < 0.0f && limits.Y > Level.Loaded.Size.X)
|
||||
{
|
||||
//no walls found at either side, just use the initial spawnpos and hope for the best
|
||||
}
|
||||
else if (limits.X < 0)
|
||||
{
|
||||
//no wall found at the left side, spawn to the left from the right-side wall
|
||||
spawnPos.X = limits.Y - minWidth * 0.5f - 100.0f + subDockingPortOffset;
|
||||
}
|
||||
else if (limits.Y > Level.Loaded.Size.X)
|
||||
{
|
||||
//no wall found at right side, spawn to the right from the left-side wall
|
||||
spawnPos.X = limits.X + minWidth * 0.5f + 100.0f + subDockingPortOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
//walls found at both sides, use their midpoint
|
||||
spawnPos.X = (limits.X + limits.Y) / 2 + subDockingPortOffset;
|
||||
}
|
||||
return spawnPos;
|
||||
}
|
||||
|
||||
spawnPos.Y = MathHelper.Clamp(spawnPos.Y, dockedBorders.Height / 2 + 10, Level.Loaded.Size.Y - dockedBorders.Height / 2 - padding * 2);
|
||||
@@ -929,11 +946,17 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// check visibility between two points (in sim units)
|
||||
/// Check visibility between two points (in sim units).
|
||||
/// </summary>
|
||||
/// <returns>a physics body that was between the points (or null)</returns>
|
||||
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true)
|
||||
///
|
||||
|
||||
/// <param name="ignoreBranches">Should plants' branches be ignored?</param>
|
||||
/// <param name="blocksVisibilityPredicate">If the predicate returns false, the fixture is ignored even if it would normally block visibility.</param>
|
||||
/// <returns>A physics body that was between the points (or null)</returns>
|
||||
public static Body CheckVisibility(Vector2 rayStart, Vector2 rayEnd, bool ignoreLevel = false, bool ignoreSubs = false, bool ignoreSensors = true, bool ignoreDisabledWalls = true, bool ignoreBranches = true,
|
||||
Predicate<Fixture> blocksVisibilityPredicate = null)
|
||||
{
|
||||
Body closestBody = null;
|
||||
float closestFraction = 1.0f;
|
||||
@@ -968,7 +991,10 @@ namespace Barotrauma
|
||||
if (sectionIndex > -1 && structure.SectionBodyDisabled(sectionIndex)) { return -1; }
|
||||
}
|
||||
}
|
||||
|
||||
if (blocksVisibilityPredicate != null && !blocksVisibilityPredicate(fixture))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (fraction < closestFraction)
|
||||
{
|
||||
closestBody = fixture.Body;
|
||||
@@ -1885,12 +1911,11 @@ namespace Barotrauma
|
||||
Unloading = true;
|
||||
try
|
||||
{
|
||||
|
||||
#if CLIENT
|
||||
RoundSound.RemoveAllRoundSounds();
|
||||
GameMain.LightManager?.ClearLights();
|
||||
depthSortedDamageable.Clear();
|
||||
#endif
|
||||
|
||||
var _loaded = new List<Submarine>(loaded);
|
||||
foreach (Submarine sub in _loaded)
|
||||
{
|
||||
@@ -1925,9 +1950,11 @@ namespace Barotrauma
|
||||
|
||||
Ragdoll.RemoveAll();
|
||||
PhysicsBody.RemoveAll();
|
||||
StatusEffect.StopAll();
|
||||
GameMain.World = null;
|
||||
|
||||
Powered.Grids.Clear();
|
||||
Powered.ChangedConnections.Clear();
|
||||
|
||||
GC.Collect();
|
||||
|
||||
@@ -1947,6 +1974,7 @@ namespace Barotrauma
|
||||
|
||||
outdoorNodes?.Clear();
|
||||
outdoorNodes = null;
|
||||
obstructedNodes.Clear();
|
||||
|
||||
GameMain.GameSession?.Campaign?.UpgradeManager?.OnUpgradesChanged?.TryDeregister(upgradeEventIdentifier);
|
||||
|
||||
@@ -1958,11 +1986,17 @@ namespace Barotrauma
|
||||
|
||||
visibleEntities = null;
|
||||
|
||||
bodyDist.Clear();
|
||||
bodies.Clear();
|
||||
|
||||
if (MainSub == this) { MainSub = null; }
|
||||
if (MainSubs[1] == this) { MainSubs[1] = null; }
|
||||
|
||||
ConnectedDockingPorts?.Clear();
|
||||
|
||||
Powered.ChangedConnections.Clear();
|
||||
Powered.Grids.Clear();
|
||||
|
||||
loaded.Remove(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,12 @@ namespace Barotrauma
|
||||
|
||||
bool hasCollider = wall.HasBody && !wall.IsPlatform && wall.StairDirection == Direction.None;
|
||||
Rectangle rect = wall.Rect;
|
||||
SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider);
|
||||
|
||||
var transformedQuad = wall.GetTransformedQuad();
|
||||
AddPointToExtents(transformedQuad.A, hasCollider: hasCollider);
|
||||
AddPointToExtents(transformedQuad.B, hasCollider: hasCollider);
|
||||
AddPointToExtents(transformedQuad.C, hasCollider: hasCollider);
|
||||
AddPointToExtents(transformedQuad.D, hasCollider: hasCollider);
|
||||
if (hasCollider)
|
||||
{
|
||||
farseerBody.CreateRectangle(
|
||||
@@ -188,7 +193,8 @@ namespace Barotrauma
|
||||
if (hull.Submarine != submarine || hull.IdFreed) { continue; }
|
||||
|
||||
Rectangle rect = hull.Rect;
|
||||
SetExtents(new Vector2(rect.X, rect.Y - rect.Height), new Vector2(rect.Right, rect.Y), hasCollider: true);
|
||||
AddPointToExtents(new Vector2(rect.X, rect.Y - rect.Height), hasCollider: true);
|
||||
AddPointToExtents(new Vector2(rect.Right, rect.Y), hasCollider: true);
|
||||
|
||||
farseerBody.CreateRectangle(
|
||||
ConvertUnits.ToSimUnits(rect.Width),
|
||||
@@ -221,33 +227,42 @@ namespace Barotrauma
|
||||
float simWidth = ConvertUnits.ToSimUnits(width);
|
||||
float simHeight = ConvertUnits.ToSimUnits(height);
|
||||
|
||||
if (radius > 0f || (width > 0f && height > 0f))
|
||||
{
|
||||
var transformedQuad = item.GetTransformedQuad();
|
||||
AddPointToExtents(transformedQuad.A, hasCollider: true);
|
||||
AddPointToExtents(transformedQuad.B, hasCollider: true);
|
||||
AddPointToExtents(transformedQuad.C, hasCollider: true);
|
||||
AddPointToExtents(transformedQuad.D, hasCollider: true);
|
||||
}
|
||||
|
||||
if (width > 0.0f && height > 0.0f)
|
||||
{
|
||||
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simHeight, 5.0f, simPos, collisionCategory, collidesWith));
|
||||
SetExtents(item.Position - new Vector2(width, height) / 2, item.Position + new Vector2(width, height) / 2, hasCollider: true);
|
||||
AddPointToExtents(item.Position - new Vector2(width, height) / 2, hasCollider: true);
|
||||
AddPointToExtents(item.Position + new Vector2(width, height) / 2, hasCollider: true);
|
||||
}
|
||||
else if (radius > 0.0f && width > 0.0f)
|
||||
{
|
||||
item.StaticFixtures.Add(farseerBody.CreateRectangle(simWidth, simRadius * 2, 5.0f, simPos, collisionCategory, collidesWith));
|
||||
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
|
||||
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitX * simWidth / 2, collisionCategory, collidesWith));
|
||||
SetExtents(item.Position - new Vector2(width / 2 + radius, height / 2), item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true);
|
||||
AddPointToExtents(item.Position - new Vector2(width / 2 + radius, height / 2), hasCollider: true);
|
||||
AddPointToExtents(item.Position + new Vector2(width / 2 + radius, height / 2), hasCollider: true);
|
||||
}
|
||||
else if (radius > 0.0f && height > 0.0f)
|
||||
{
|
||||
item.StaticFixtures.Add(farseerBody.CreateRectangle(simRadius * 2, height, 5.0f, simPos, collisionCategory, collidesWith));
|
||||
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos - Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
|
||||
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos + Vector2.UnitY * simHeight / 2, collisionCategory, collidesWith));
|
||||
SetExtents(item.Position - new Vector2(width / 2, height / 2 + radius), item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true);
|
||||
AddPointToExtents(item.Position - new Vector2(width / 2, height / 2 + radius), hasCollider: true);
|
||||
AddPointToExtents(item.Position + new Vector2(width / 2, height / 2 + radius), hasCollider: true);
|
||||
}
|
||||
else if (radius > 0.0f)
|
||||
{
|
||||
item.StaticFixtures.Add(farseerBody.CreateCircle(simRadius, 5.0f, simPos, collisionCategory, collidesWith));
|
||||
visibleMinExtents.X = Math.Min(item.Position.X - radius, visibleMinExtents.X);
|
||||
visibleMinExtents.Y = Math.Min(item.Position.Y - radius, visibleMinExtents.Y);
|
||||
visibleMaxExtents.X = Math.Max(item.Position.X + radius, visibleMaxExtents.X);
|
||||
visibleMaxExtents.Y = Math.Max(item.Position.Y + radius, visibleMaxExtents.Y);
|
||||
SetExtents(item.Position - new Vector2(radius, radius), item.Position + new Vector2(radius, radius), hasCollider: true);
|
||||
AddPointToExtents(item.Position - new Vector2(radius, radius), hasCollider: true);
|
||||
AddPointToExtents(item.Position + new Vector2(radius, radius), hasCollider: true);
|
||||
}
|
||||
item.StaticFixtures.ForEach(f => f.UserData = item);
|
||||
}
|
||||
@@ -268,18 +283,18 @@ namespace Barotrauma
|
||||
|
||||
Body = new PhysicsBody(farseerBody);
|
||||
|
||||
void SetExtents(Vector2 min, Vector2 max, bool hasCollider)
|
||||
void AddPointToExtents(Vector2 point, bool hasCollider)
|
||||
{
|
||||
visibleMinExtents.X = Math.Min(min.X, visibleMinExtents.X);
|
||||
visibleMinExtents.Y = Math.Min(min.Y, visibleMinExtents.Y);
|
||||
visibleMaxExtents.X = Math.Max(max.X, visibleMaxExtents.X);
|
||||
visibleMaxExtents.Y = Math.Max(max.Y, visibleMaxExtents.Y);
|
||||
visibleMinExtents.X = Math.Min(point.X, visibleMinExtents.X);
|
||||
visibleMinExtents.Y = Math.Min(point.Y, visibleMinExtents.Y);
|
||||
visibleMaxExtents.X = Math.Max(point.X, visibleMaxExtents.X);
|
||||
visibleMaxExtents.Y = Math.Max(point.Y, visibleMaxExtents.Y);
|
||||
if (hasCollider)
|
||||
{
|
||||
minExtents.X = Math.Min(min.X, minExtents.X);
|
||||
minExtents.Y = Math.Min(min.Y, minExtents.Y);
|
||||
maxExtents.X = Math.Max(max.X, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(max.Y, maxExtents.Y);
|
||||
minExtents.X = Math.Min(point.X, minExtents.X);
|
||||
minExtents.Y = Math.Min(point.Y, minExtents.Y);
|
||||
maxExtents.X = Math.Max(point.X, maxExtents.X);
|
||||
maxExtents.Y = Math.Max(point.Y, maxExtents.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Steamworks.ServerList;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -111,7 +110,7 @@ namespace Barotrauma
|
||||
|
||||
public void OnSpawned(Entity spawnedItem)
|
||||
{
|
||||
if (!(spawnedItem is Item item)) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); }
|
||||
if (spawnedItem is not Item item) { throw new ArgumentException($"The entity passed to ItemSpawnInfo.OnSpawned must be an Item (value was {spawnedItem?.ToString() ?? "null"})."); }
|
||||
onSpawned?.Invoke(item);
|
||||
}
|
||||
}
|
||||
@@ -443,6 +442,7 @@ namespace Barotrauma
|
||||
CreateNetworkEventProjSpecific(new SpawnEntity(spawnedEntity));
|
||||
}
|
||||
spawnInfo.OnSpawned(spawnedEntity);
|
||||
GameMain.GameSession?.EventManager?.EntitySpawned(spawnedEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace Barotrauma.Networking
|
||||
int orderPriority = msg.ReadByte();
|
||||
OrderTarget orderTargetPosition = null;
|
||||
Order.OrderTargetType orderTargetType = (Order.OrderTargetType)msg.ReadByte();
|
||||
int wallSectionIndex = 0;
|
||||
int? wallSectionIndex = null;
|
||||
if (msg.ReadBoolean())
|
||||
{
|
||||
float x = msg.ReadSingle();
|
||||
|
||||
@@ -252,6 +252,13 @@ namespace Barotrauma.Networking
|
||||
continue;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
foreach (var itemComponent in item.Components)
|
||||
{
|
||||
itemComponent.StopLoopingSound();
|
||||
}
|
||||
#endif
|
||||
|
||||
//restore other items to full condition and recharge batteries
|
||||
item.Condition = item.MaxCondition;
|
||||
item.GetComponent<Repairable>()?.ResetDeterioration();
|
||||
|
||||
@@ -848,6 +848,13 @@ namespace Barotrauma.Networking
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(10.0f, IsPropertySaveable.Yes)]
|
||||
public float MinimumMidRoundSyncTimeout
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private bool karmaEnabled;
|
||||
[Serialize(false, IsPropertySaveable.Yes)]
|
||||
public bool KarmaEnabled
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user