Faction Test 100.13.0.0

This commit is contained in:
Markus Isberg
2023-01-11 15:36:23 +02:00
parent 1650735468
commit caa5a2f762
145 changed files with 2100 additions and 1111 deletions
@@ -11,6 +11,7 @@
RadiationEnabled="false"
StartingBalanceAmount="High"
StartItemSet="easy"
MaxMissionCount="3"
Difficulty="Easy"/>
<CampaignSettings
presetname="Normal"
@@ -18,12 +19,14 @@
RadiationEnabled="false"
StartingBalanceAmount="Medium"
StartItemSet="normal"
MaxMissionCount="2"
Difficulty="Medium"/>
<CampaignSettings
presetname="Hard"
TutorialEnabled="false"
RadiationEnabled="true"
RadiationEnabled="false"
StartingBalanceAmount="Low"
StartItemSet="hard"
MaxMissionCount="1"
Difficulty="Hard"/>
</CampaignSettingPresets>
@@ -352,6 +352,21 @@ namespace Barotrauma
}
}
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
return myTeam switch
{
// NPCs are friendly to the same team and the friendly NPCs
CharacterTeamType.None or CharacterTeamType.Team1 or CharacterTeamType.Team2 => otherTeam == CharacterTeamType.FriendlyNPC,
// Friendly NPCs are friendly to both player teams
CharacterTeamType.FriendlyNPC => otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2,
_ => true
};
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public void ReequipUnequipped()
{
foreach (var item in unequippedItems)
@@ -354,6 +354,10 @@ namespace Barotrauma
{
targetingTag = "owner";
}
else if (targetCharacter.AIController is HumanAIController && !IsOnFriendlyTeam(Character, targetCharacter))
{
targetingTag = "hostile";
}
else if (AIParams.TryGetTarget(targetCharacter, out CharacterParams.TargetParams tP))
{
targetingTag = tP.Tag;
@@ -364,7 +368,7 @@ namespace Barotrauma
{
targetingTag = "husk";
}
else if (!Character.IsFriendly(targetCharacter))
else if (!Character.IsSameSpeciesOrGroup(targetCharacter))
{
if (enemy.CombatStrength > CombatStrength)
{
@@ -689,12 +693,9 @@ namespace Barotrauma
return a.Damage >= selectedTargetingParams.Threshold;
}
Character attacker = targetCharacter.LastAttackers.LastOrDefault(IsValid)?.Character;
//if the attacker has the same targeting tag as the character we're protecting, we can't change the TargetState
//otherwise e.g. a pet that's set to follow humans would start attacking all humans (and other pets, since they're considered part of the same group) when a hostile human attacks it
//TODO: a way for pets to differentiate hostile and friendly humans?
if (attacker?.AiTarget != null && targetCharacter.SpeciesName != GetTargetingTag(attacker.AiTarget) && !attacker.IsFriendly(targetCharacter))
if (attacker?.AiTarget != null && !Character.IsSameSpeciesOrGroup(attacker) && !targetCharacter.IsSameSpeciesOrGroup(attacker))
{
// Attack the character that attacked the target we are protecting
// Can't retaliate on characters of same species or group because that would make us hostile to all friendly characters in the same group.
ChangeTargetState(attacker, AIState.Attack, selectedTargetingParams.Priority * 2);
SelectTarget(attacker.AiTarget);
State = AIState.Attack;
@@ -1514,9 +1514,18 @@ namespace Barotrauma
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
//do a raycast upwards to find any walls
float minCeilingDist = Character.AnimController.Collider.Height / 2 + Character.AnimController.Collider.Radius + 0.1f;
if (!Character.AnimController.TryGetCollider(0, out PhysicsBody mainCollider))
{
mainCollider = Character.AnimController.Collider;
}
float margin = 0.1f;
if (shouldCrouch)
{
margin *= 2;
}
float minCeilingDist = mainCollider.Height / 2 + mainCollider.Radius + margin;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return fixture.Body.UserData is not Submarine; }) != null;
}
public bool AllowCampaignInteraction()
@@ -1589,7 +1598,27 @@ namespace Barotrauma
(!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, matchingItems);
items = matchingItems;
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
foreach (var item in matchingItems)
{
if (item == null) { continue; }
if (containedTag.IsEmpty || item.OwnInventory == null)
{
//no contained items required, this item's ok
return true;
}
var suitableSlot = item.GetComponent<ItemContainer>().FindSuitableSubContainerIndex(containedTag);
if (suitableSlot == null)
{
//no restrictions on the suitable slot
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage);
}
else
{
return item.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage && it.ParentInventory.IsInSlot(it, suitableSlot.Value));
}
}
return false;
}
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
@@ -2016,11 +2045,9 @@ namespace Barotrauma
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameTeam = me.TeamID == other.TeamID;
bool friendlyTeam = IsOnFriendlyTeam(me, other);
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
bool teamGood = sameTeam || !onlySameTeam && IsOnFriendlyTeam(me, other);
if (!teamGood) { return false; }
bool speciesGood = other.IsPet || other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
if (!speciesGood) { return false; }
if (!me.IsSameSpeciesOrGroup(other)) { return false; }
if (me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
@@ -2029,30 +2056,14 @@ namespace Barotrauma
return false;
}
}
if (!sameTeam && me.TeamID == CharacterTeamType.None && other.IsPet)
{
// Hostile NPCs are hostile to all pets, unless they are in the same team.
return false;
}
return true;
}
public static bool IsOnFriendlyTeam(CharacterTeamType myTeam, CharacterTeamType otherTeam)
{
if (myTeam == otherTeam) { return true; }
switch (myTeam)
{
case CharacterTeamType.None:
case CharacterTeamType.Team1:
case CharacterTeamType.Team2:
// Only friendly to the same team and friendly NPCs
return otherTeam == CharacterTeamType.FriendlyNPC;
case CharacterTeamType.FriendlyNPC:
// Friendly NPCs are friendly to both teams
return otherTeam == CharacterTeamType.Team1 || otherTeam == CharacterTeamType.Team2;
default:
return true;
}
}
public static bool IsOnFriendlyTeam(Character me, Character other) => IsOnFriendlyTeam(me.TeamID, other.TeamID);
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
@@ -83,6 +83,7 @@ namespace Barotrauma
{
if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null &&
GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
@@ -98,7 +98,7 @@ namespace Barotrauma
int containedItemCount = 0;
foreach (Item it in container.Inventory.AllItems)
{
if (CheckItem(it))
if (CheckItem(it) && IsInTargetSlot(it))
{
containedItemCount++;
}
@@ -118,7 +118,11 @@ namespace Barotrauma
Abandon = true;
return;
}
ItemToContain = item ?? character.Inventory.FindItem(i => CheckItem(i) && i.Container != container.Item, recursive: true);
ItemToContain = item ?? character.Inventory.FindItem(it =>
CheckItem(it) &&
//ignore items already in the container, unless we're trying to place to a specific slot, and the item's not in it
(it.Container != container.Item || (TargetSlot.HasValue && it.Container.OwnInventory.FindIndex(it) != TargetSlot)),
recursive: true);
if (ItemToContain != null)
{
if (!character.CanInteractWith(ItemToContain, checkLinked: false))
@@ -1,5 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
@@ -18,6 +18,7 @@ namespace Barotrauma
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
private Item targetItem;
private int? oxygenSourceSlotIndex;
public const float MIN_OXYGEN = 10;
@@ -43,12 +44,15 @@ namespace Barotrauma
Abandon = true;
return;
}
targetItem = character.Inventory.FindItemByTag(gearTag, true);
TrySetTargetItem(character.Inventory.FindItemByTag(gearTag, true));
if (targetItem == null && gearTag == LIGHT_DIVING_GEAR)
{
targetItem = character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true);
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
}
if (targetItem == null || !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && targetItem.ContainedItems.Any(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > 0))
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{
TryAddSubObjective(ref getDivingGear, () =>
{
@@ -84,7 +88,7 @@ namespace Barotrauma
else
{
float min = GetMinOxygen(character);
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > min))
if (targetItem.OwnInventory != null && targetItem.OwnInventory.AllItems.None(it => IsSuitableContainedOxygenSource(it)))
{
TryAddSubObjective(ref getOxygen, () =>
{
@@ -93,7 +97,7 @@ namespace Barotrauma
if (HumanAIController.HasItem(character, OXYGEN_SOURCE, out _, conditionPercentage: min))
{
character.Speak(TextManager.Get("dialogswappingoxygentank").Value, null, 0, "swappingoxygentank".ToIdentifier(), 30.0f);
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min).Count == 1)
if (character.Inventory.FindAllItems(i => i.HasTag(OXYGEN_SOURCE) && i.Condition > min, recursive: true).Count == 1)
{
character.Speak(TextManager.Get("dialoglastoxygentank").Value, null, 0.0f, "dialoglastoxygentank".ToIdentifier(), 30.0f);
}
@@ -109,7 +113,8 @@ namespace Barotrauma
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN,
RemoveExistingWhenNecessary = true
RemoveExistingWhenNecessary = true,
TargetSlot = oxygenSourceSlotIndex
};
if (container.HasSubContainers)
{
@@ -167,12 +172,36 @@ namespace Barotrauma
}
}
private bool IsSuitableContainedOxygenSource(Item item)
{
return
item != null &&
item.HasTag(OXYGEN_SOURCE) &&
item.Condition > 0 &&
(oxygenSourceSlotIndex == null || item.ParentInventory.IsInSlot(item, oxygenSourceSlotIndex.Value));
}
private void TrySetTargetItem(Item item)
{
if (targetItem == item) { return; }
targetItem = item;
if (targetItem != null)
{
oxygenSourceSlotIndex = targetItem.GetComponent<ItemContainer>()?.FindSuitableSubContainerIndex(OXYGEN_SOURCE);
}
else
{
oxygenSourceSlotIndex = null;
}
}
public override void Reset()
{
base.Reset();
getDivingGear = null;
getOxygen = null;
targetItem = null;
oxygenSourceSlotIndex = null;
}
public static float GetMinOxygen(Character character)
@@ -71,7 +71,7 @@ namespace Barotrauma
Priority = 100;
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !HumanAIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
character.Submarine != null && !AIController.IsOnFriendlyTeam(character.TeamID, character.Submarine.TeamID))
{
// Ordered to follow, hold position, or return back to main sub inside a hostile sub
// -> ignore find safety unless we need to find a diving gear
@@ -413,7 +413,7 @@ namespace Barotrauma
}
}
}
private void ApplyTreatment(Affliction affliction, Item item)
{
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
@@ -412,7 +412,8 @@ namespace Barotrauma
private int CalculateCellCount(int minValue, int maxValue)
{
if (maxValue == 0) { return 0; }
float t = MathUtils.InverseLerp(0, 100, Level.Loaded.Difficulty * Config.AgentSpawnCountDifficultyMultiplier);
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
float t = MathUtils.InverseLerp(0, 100, difficulty * Config.AgentSpawnCountDifficultyMultiplier);
return (int)Math.Round(MathHelper.Lerp(minValue, maxValue, t));
}
@@ -422,7 +423,8 @@ namespace Barotrauma
float delay = Config.AgentSpawnDelay;
float min = delay;
float max = delay * 6;
float t = Level.Loaded.Difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
float difficulty = Level.Loaded?.Difficulty ?? 0.0f;
float t = difficulty * Config.AgentSpawnDelayDifficultyMultiplier * Rand.Range(1 - randomFactor, 1 + randomFactor);
return MathHelper.Lerp(max, min, MathUtils.InverseLerp(0, 100, t));
}
@@ -24,7 +24,7 @@ namespace Barotrauma
public bool IsAiming => wasAiming;
public bool IsAimingMelee => wasAimingMelee;
protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
protected bool Aiming => aiming || aimingMelee || FlipLockTime > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
public float ArmLength => upperArmLength + forearmLength;
@@ -278,7 +278,11 @@ namespace Barotrauma
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
public float LockFlippingUntil;
public float FlipLockTime { get; private set; }
public void LockFlipping(float time = 0.2f)
{
FlipLockTime = (float)Timing.TotalTime + time;
}
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
@@ -1023,7 +1023,7 @@ namespace Barotrauma
foreach (Limb l in Limbs)
{
if (l.IsSevered) { continue; }
if (!l.DoesFlip) { continue; }
if (!l.DoesFlip) { continue; }
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
//horizontally aligned limbs need to be flipped 180 degrees
@@ -1043,7 +1043,7 @@ namespace Barotrauma
if (l.IsSevered) { continue; }
float rotation = l.body.Rotation;
if (l.DoesFlip)
if (l.DoesMirror)
{
if (RagdollParams.IsSpritesheetOrientationHorizontal)
{
@@ -431,7 +431,7 @@ namespace Barotrauma
}
}
if (Timing.TotalTime > LockFlippingUntil && TargetDir != dir && !IsStuck)
if (Timing.TotalTime > FlipLockTime && TargetDir != dir && !IsStuck)
{
Flip();
}
@@ -1723,7 +1723,7 @@ namespace Barotrauma
{
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
{
target.AnimController.LockFlippingUntil = (float)Timing.TotalTime + 0.5f;
target.AnimController.LockFlipping(0.5f);
}
else
{
@@ -1822,16 +1822,22 @@ namespace Barotrauma
public override void Flip()
{
if (Character == null || Character.Removed)
{
LogAccessedRemovedCharacterError();
return;
}
base.Flip();
WalkPos = -WalkPos;
Limb torso = GetLimb(LimbType.Torso);
Vector2 difference;
if (torso == null) { return; }
Matrix torsoTransform = Matrix.CreateRotationZ(torso.Rotation);
Vector2 difference;
foreach (Item heldItem in character.HeldItems)
{
if (heldItem?.body != null && !heldItem.Removed && heldItem.GetComponent<Holdable>() != null)
@@ -57,17 +57,7 @@ namespace Barotrauma
{
if (limbs == null)
{
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
LogAccessedRemovedCharacterError();
return Array.Empty<Limb>();
}
return limbs;
@@ -158,6 +148,20 @@ namespace Barotrauma
}
}
public bool TryGetCollider(int index, out PhysicsBody collider)
{
collider = null;
try
{
collider = this.collider?[index];
return true;
}
catch
{
return false;
}
}
public int ColliderIndex
{
get
@@ -881,7 +885,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; }
if (limb == null || limb.IsSevered || !limb.DoesMirror) { continue; }
limb.Dir = Dir;
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
limb.MirrorPullJoint();
@@ -1436,6 +1440,21 @@ namespace Barotrauma
return true;
}
protected void LogAccessedRemovedCharacterError()
{
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll:AccessRemoved",
GameAnalyticsManager.ErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.SpeciesName + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace.CleanupStackTrace());
accessRemovedCharacterErrorShown = true;
}
}
partial void UpdateProjSpecific(float deltaTime, Camera cam);
partial void Splash(Limb limb, Hull limbHull);
@@ -498,37 +498,52 @@ namespace Barotrauma
DamageParticles(deltaTime, worldPosition);
var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult();
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
var additionalEffectType = ActionType.OnUse;
if (targetCharacter != null && targetCharacter.IsDead)
{
effectType = ActionType.OnEating;
additionalEffectType = ActionType.OnEating;
}
foreach (StatusEffect effect in statusEffects)
{
effect.sourceBody = sourceBody;
if (effect.HasTargetType(StatusEffect.TargetType.This))
if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
{
// TODO: do we want to apply the effect at the world position or the entity positions in each cases? -> go through also other cases where status effects are applied
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity, worldPosition);
var t = sourceLimb ?? attacker as ISerializableEntity;
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, attacker, t, worldPosition);
}
effect.Apply(additionalEffectType, deltaTime, attacker, t, worldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
}
effect.Apply(additionalEffectType, deltaTime, attacker, attacker);
}
if (targetCharacter != null)
{
if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(effectType, deltaTime, targetCharacter, attackResult.HitLimb);
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
}
effect.Apply(additionalEffectType, deltaTime, targetCharacter, attackResult.HitLimb);
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
effect.Apply(effectType, deltaTime, targetCharacter, targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
// TODO: do we need the conversion to list here? It generates garbage.
var targets = targetCharacter.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetCharacter, targets);
}
effect.Apply(additionalEffectType, deltaTime, targetCharacter, targets);
}
}
if (target is Entity targetEntity)
@@ -538,18 +553,30 @@ namespace Barotrauma
{
targets.Clear();
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetEntity, targets);
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targets);
}
effect.Apply(additionalEffectType, deltaTime, targetEntity, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition);
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
}
effect.Apply(additionalEffectType, deltaTime, targetEntity, targetEntity as ISerializableEntity, worldPosition);
}
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
if (additionalEffectType != ActionType.OnEating)
{
effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
}
effect.Apply(additionalEffectType, deltaTime, attacker, targets);
}
}
@@ -585,47 +612,52 @@ namespace Barotrauma
}
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
var effectType = attackResult.Damage > 0.0f ? ActionType.OnUse : ActionType.OnFailure;
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
{
effect.sourceBody = sourceBody;
if (effect.HasTargetType(StatusEffect.TargetType.This))
if (effect.HasTargetType(StatusEffect.TargetType.This) || effect.HasTargetType(StatusEffect.TargetType.Character))
{
effect.Apply(effectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
effect.Apply(conditionalEffectType, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
effect.Apply(ActionType.OnUse, deltaTime, attacker, sourceLimb ?? attacker as ISerializableEntity);
}
if (effect.HasTargetType(StatusEffect.TargetType.Parent))
{
effect.Apply(effectType, deltaTime, attacker, attacker);
effect.Apply(conditionalEffectType, deltaTime, attacker, attacker);
effect.Apply(ActionType.OnUse, deltaTime, attacker, attacker);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character))
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character);
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb.character);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb.character);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb);
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targetLimb);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targetLimb);
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
// TODO: do we need the conversion to list here? It generates garbage.
var targets = targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList();
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
effect.AddNearbyTargets(worldPosition, targets);
effect.Apply(effectType, deltaTime, targetLimb.character, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
effect.Apply(conditionalEffectType, deltaTime, targetLimb.character, targets);
effect.Apply(ActionType.OnUse, deltaTime, targetLimb.character, targets);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
effect.Apply(conditionalEffectType, deltaTime, attacker, targets);
effect.Apply(ActionType.OnUse, deltaTime, attacker, targets);
}
}
@@ -365,9 +365,15 @@ namespace Barotrauma
public readonly CharacterPrefab Prefab;
public readonly CharacterParams Params;
public Identifier SpeciesName => Params?.SpeciesName ?? "null".ToIdentifier();
public Identifier Group => HumanPrefab is HumanPrefab humanPrefab && !humanPrefab.Group.IsEmpty ? humanPrefab.Group : Params.Group;
public bool IsHumanoid => Params.Humanoid;
public bool IsMachine => Params.IsMachine;
public bool IsHusk => Params.Husk;
public bool IsMale => info?.IsMale ?? false;
@@ -1613,7 +1619,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\".");
}
else if (humanPrefab.GiveItems(this, Submarine, spawnPoint))
else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
{
return;
}
@@ -1752,7 +1758,7 @@ namespace Barotrauma
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier);
SpeedMultiplier = Math.Max(0.0f, greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier));
targetMovement *= SpeedMultiplier;
// Reset, status effects will set the value before the next update
ResetSpeedMultiplier();
@@ -3881,8 +3887,8 @@ namespace Barotrauma
{
foreach (Affliction affliction in attackResult.Afflictions)
{
if (affliction.Strength == 0.0f) continue;
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}");
if (Math.Abs(affliction.Strength) <= 0.1f) { continue;}
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength.ToString("0.0")}");
}
}
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
@@ -4481,7 +4487,10 @@ namespace Barotrauma
#if CLIENT
//ensure we apply any pending inventory updates to drop any items that need to be dropped when the character despawns
Inventory?.ApplyReceivedState();
if (GameMain.Client?.ClientPeer is { IsActive: true })
{
Inventory?.ApplyReceivedState();
}
#endif
base.Remove();
@@ -5197,15 +5206,13 @@ namespace Barotrauma
public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier);
/// <summary>
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary>
public bool IsFriendly(Character other) => IsFriendly(this, other);
/// <summary>
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary>
public static bool IsFriendly(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
public static bool IsFriendly(Character me, Character other) => AIController.IsOnFriendlyTeam(me, other) && IsSameSpeciesOrGroup(me, other);
public bool IsSameSpeciesOrGroup(Character other) => IsSameSpeciesOrGroup(this, other);
public static bool IsSameSpeciesOrGroup(Character me, Character other) => other.SpeciesName == me.SpeciesName || CharacterParams.CompareGroup(me.Group, other.Group);
public void StopClimbing()
{
@@ -779,9 +779,10 @@ namespace Barotrauma
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.White);
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.White);
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White);
//default to transparent color, it's invalid and will be replaced with a random one in CheckColors
Head.SkinColor = infoElement.GetAttributeColor("skincolor", Color.Transparent);
Head.HairColor = infoElement.GetAttributeColor("haircolor", Color.Transparent);
Head.FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Transparent);
CheckColors();
TryLoadNameAndTitle(npcIdentifier);
@@ -146,12 +146,6 @@ namespace Barotrauma
{
return minVitality;
}
if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
return Math.Max(vitality, MinVitality + 1);
}
return vitality;
}
@@ -587,6 +581,15 @@ namespace Barotrauma
}
}
private void KillIfOutOfVitality()
{
if (Vitality <= MinVitality &&
!Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
Kill();
}
}
private readonly static List<Affliction> afflictionsToRemove = new List<Affliction>();
private readonly static List<KeyValuePair<Affliction, LimbHealth>> afflictionsToUpdate = new List<KeyValuePair<Affliction, LimbHealth>>();
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
@@ -611,7 +614,7 @@ namespace Barotrauma
}
CalculateVitality();
if (Vitality <= MinVitality) { Kill(); }
KillIfOutOfVitality();
}
public float GetLimbDamage(Limb limb, string afflictionType = null)
@@ -729,10 +732,7 @@ namespace Barotrauma
existingAffliction.Duration = existingAffliction.Prefab.Duration;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
KillIfOutOfVitality();
return;
}
@@ -746,10 +746,7 @@ namespace Barotrauma
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
KillIfOutOfVitality();
#if CLIENT
if (OpenHealthWindow != this && limbHealth != null)
{
@@ -844,11 +841,7 @@ namespace Barotrauma
}
#endif
CalculateVitality();
if (Vitality <= MinVitality)
{
Kill();
}
KillIfOutOfVitality();
}
}
@@ -879,7 +872,11 @@ namespace Barotrauma
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsOxygen) { return; }
if (!Character.NeedsOxygen)
{
oxygenLowAffliction.Strength = 0.0f;
return;
}
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
float prevOxygen = OxygenAmount;
@@ -1025,17 +1022,18 @@ namespace Barotrauma
}
private readonly List<Affliction> allAfflictions = new List<Affliction>();
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions)
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions, Func<Affliction, bool> predicate = null)
{
allAfflictions.Clear();
if (!mergeSameAfflictions)
{
allAfflictions.AddRange(afflictions.Keys);
allAfflictions.AddRange(predicate == null ? afflictions.Keys : afflictions.Keys.Where(predicate));
}
else
{
foreach (Affliction affliction in afflictions.Keys)
{
if (predicate != null && !predicate(affliction)) { continue; }
var existingAffliction = allAfflictions.Find(a => a.Prefab == affliction.Prefab);
if (existingAffliction == null)
{
@@ -200,7 +200,7 @@ namespace Barotrauma
}
}
}
partial class Limb : ISerializableEntity, ISpatialEntity
{
//how long it takes for severed limbs to fade out
@@ -215,7 +215,7 @@ namespace Barotrauma
//the physics body of the limb
public PhysicsBody body;
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
public Hull Hull;
@@ -249,7 +249,7 @@ namespace Barotrauma
}
}
}
private bool isSevered;
private float severedFadeOutTimer;
@@ -269,7 +269,7 @@ namespace Barotrauma
mouthPos = value;
}
}
public readonly Attack attack;
public List<DamageModifier> DamageModifiers { get; private set; } = new List<DamageModifier>();
@@ -282,39 +282,73 @@ namespace Barotrauma
{
get
{
if (character.AnimController.CurrentAnimationParams is GroundedMovementParams)
if (character?.AnimController.CurrentAnimationParams is GroundedMovementParams && IsLeg)
{
switch (type)
{
case LimbType.LeftFoot:
case LimbType.LeftLeg:
case LimbType.LeftThigh:
case LimbType.RightFoot:
case LimbType.RightLeg:
case LimbType.RightThigh:
// Legs always has to flip
return true;
}
// Legs always has to flip when not swimming
return true;
}
return Params.Flip;
}
}
public bool DoesMirror
{
get
{
if (IsLeg)
{
// Legs always has to mirror
return true;
}
return DoesFlip;
}
}
public float SteerForce => Params.SteerForce;
public Vector2 DebugTargetPos;
public Vector2 DebugRefPos;
public bool IsLowerBody =>
type == LimbType.LeftLeg ||
type == LimbType.RightLeg ||
type == LimbType.LeftFoot ||
type == LimbType.RightFoot ||
type == LimbType.Tail ||
type == LimbType.Legs ||
type == LimbType.RightThigh ||
type == LimbType.LeftThigh ||
type == LimbType.Waist;
public bool IsLowerBody
{
get
{
switch (type)
{
case LimbType.LeftLeg:
case LimbType.RightLeg:
case LimbType.LeftFoot:
case LimbType.RightFoot:
case LimbType.Tail:
case LimbType.Legs:
case LimbType.LeftThigh:
case LimbType.RightThigh:
case LimbType.Waist:
return true;
default:
return false;
}
}
}
public bool IsLeg
{
get
{
switch (type)
{
case LimbType.LeftFoot:
case LimbType.LeftLeg:
case LimbType.LeftThigh:
case LimbType.RightFoot:
case LimbType.RightLeg:
case LimbType.RightThigh:
return true;
default:
return false;
}
}
}
public bool IsSevered
{
@@ -75,13 +75,14 @@ namespace Barotrauma.Abilities
if (wt == WeaponType.Any || !weapontype.HasFlag(wt)) { continue; }
switch (wt)
{
// it is possible that an item that has both a melee and a projectile component will return true
// even when not used as a melee/ranged weapon respectively
// attackdata should contain data regarding whether the attack is melee or not
case WeaponType.Melee:
//if the item has an active projectile component (has been fired), don't consider it a melee weapon
if (item?.GetComponent<Projectile>() is { IsActive: true }) { continue; }
if (item?.GetComponent<MeleeWeapon>() != null) { return true; }
break;
case WeaponType.Ranged:
//if the item has a melee weapon component that's being used now, don't consider it a projectile
if (item?.GetComponent<MeleeWeapon>() is { Hitting: true }) { continue; }
if (item?.GetComponent<Projectile>() != null) { return true; }
break;
case WeaponType.HandheldRanged:
@@ -17,6 +17,14 @@ namespace Barotrauma.Abilities
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (addingFirstTime)
{
VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f);
}
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched)
@@ -13,6 +13,11 @@
value = abilityElement.GetAttributeFloat("value", 0f);
}
public override void InitializeAbility(bool addingFirstTime)
{
VerifyState(conditionsMatched: true, timeSinceLastUpdate: 0.0f);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
if (conditionsMatched != lastState)
@@ -1,5 +1,6 @@
#nullable enable
using System;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -17,6 +18,9 @@ namespace Barotrauma.Abilities
{
if (!addingFirstTime) { return; }
// do not run client-side in multiplayer
if (GameMain.NetworkMember is { IsClient: true }) { return; }
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
if (apprentice is null)
{
@@ -49,6 +49,16 @@ namespace Barotrauma.Abilities
break;
}
}
switch (abilityEffectType)
{
case AbilityEffectType.OnDieToCharacter:
if (characterAbilities.Any(a => a.RequiresAlive))
{
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.");
}
break;
}
}
public void ActivateAbilityGroup(bool addingFirstTime)
@@ -23,6 +23,8 @@ namespace Barotrauma
public const string RegularPackagesElementName = "regularpackages";
public const string RegularPackagesSubElementName = "package";
public static bool ModsEnabled => GameMain.VanillaContent == null || EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent);
public static class EnabledPackages
{
public static CorePackage? Core { get; private set; } = null;
@@ -88,6 +88,7 @@ namespace Barotrauma
public T GetAttributeEnum<T>(string key, in T def) where T : struct, Enum => Element.GetAttributeEnum(key, def);
public (T1, T2) GetAttributeTuple<T1, T2>(string key, in (T1, T2) def) => Element.GetAttributeTuple(key, def);
public (T1, T2)[] GetAttributeTupleArray<T1, T2>(string key, in (T1, T2)[] def) => Element.GetAttributeTupleArray(key, def);
public Range<int> GetAttributeRange(string key, in Range<int> def) => Element.GetAttributeRange(key, def);
public Identifier VariantOf() => Element.VariantOf();
@@ -1263,6 +1263,22 @@ namespace Barotrauma
}
#endif
commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) =>
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
NewMessage("Reputation:");
foreach (var faction in campaign.Factions)
{
NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}");
}
}
else
{
ThrowError("Could not show reputation (no active campaign).");
}
}, null));
commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) =>
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
@@ -1315,10 +1331,10 @@ namespace Barotrauma
}
}, () =>
{
return new[]
{
FactionPrefab.Prefabs.Select(f => f.Identifier.Value).ToArray(),
GameMain.GameSession?.Campaign.Factions.Select(f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
return new[]
{
FactionPrefab.Prefabs.Select(static f => f.Identifier.Value).ToArray(),
GameMain.GameSession?.Campaign?.Factions.Select(static f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
};
}, true));
@@ -171,7 +171,7 @@ namespace Barotrauma
PumpSpeed,
PumpMaxFlow,
ReactorMaxOutput,
ReactorFuelEfficiency,
ReactorFuelConsumption,
DeconstructorSpeed,
FabricationSpeed
}
@@ -200,7 +200,7 @@ namespace Barotrauma
}
}
private int[] GetEndingOptions()
public int[] GetEndingOptions()
{
List<int> endings = Options.Where(group => !group.Actions.Any() || group.EndConversation).Select(group => Options.IndexOf(group)).ToList();
if (!ContinueConversation) { endings.Add(-1); }
@@ -68,7 +68,7 @@ namespace Barotrauma
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable());
if (emptyLocation != null)
{
emptyLocation.ChangeType(campaign, Barotrauma.LocationType.Prefabs[LocationTypes[0]]);
emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation;
}
}
@@ -77,7 +77,7 @@ namespace Barotrauma
{
if (!MissionIdentifier.IsEmpty)
{
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
unlockedMission = unlockLocation.UnlockMissionByIdentifier(MissionIdentifier);
}
else if (!MissionTag.IsEmpty)
{
@@ -89,8 +89,9 @@ namespace Barotrauma
}
if (unlockedMission != null)
{
unlockedMission.OriginLocation = campaign.Map.CurrentLocation;
campaign.Map.Discover(unlockLocation, checkTalents: false);
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] ==null)
if (unlockedMission.Locations[0] == unlockedMission.Locations[1] || unlockedMission.Locations[1] == null)
{
DebugConsole.NewMessage($"Unlocked mission \"{unlockedMission.Name}\" in the location \"{unlockLocation.Name}\".");
}
@@ -12,6 +12,7 @@ namespace Barotrauma
public enum NetworkEventType
{
CONVERSATION,
CONVERSATION_SELECTED_OPTION,
STATUSEFFECT,
MISSION,
UNLOCKPATH
@@ -229,9 +229,8 @@ namespace Barotrauma
}
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos, giveTags: true);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
@@ -240,6 +239,7 @@ namespace Barotrauma
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
}
}
if (spawnPos is WayPoint wp)
{
spawnedCharacter.GiveIdCardTags(wp);
@@ -94,7 +94,7 @@ namespace Barotrauma
List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
foreach (Item item in Item.ItemList)
{
if (!connectedSubs.Contains(item.Submarine)) { continue; }
if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; }
if (item.GetComponent<PowerTransfer>() != null ||
item.GetComponent<PowerContainer>() != null ||
item.GetComponent<Reactor>() != null ||
@@ -125,6 +125,12 @@ namespace Barotrauma
public Identifier SonarIconIdentifier => Prefab.SonarIconIdentifier;
/// <summary>
/// Where was this mission received from? Affects which faction we give reputation for if the mission is configured to give reputation for the faction that gave the mission.
/// Defaults to Locations[0]
/// </summary>
public Location OriginLocation;
public readonly Location[] Locations;
public int? Difficulty
@@ -144,7 +150,7 @@ namespace Barotrauma
}
}
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Action<Mission> OnMissionStateChanged;
@@ -160,12 +166,13 @@ namespace Barotrauma
Headers = prefab.Headers;
var messages = prefab.Messages.ToArray();
OriginLocation = locations[0];
Locations = locations;
var endConditionElement = prefab.ConfigElement.GetChildElement(nameof(completeCheckDataAction));
if (endConditionElement != null)
{
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier.ToString()})");
completeCheckDataAction = new CheckDataAction(endConditionElement, $"Mission ({prefab.Identifier})");
}
for (int n = 0; n < 2; n++)
@@ -407,7 +414,7 @@ namespace Barotrauma
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value));
info?.GiveExperience((int)((experienceGain * experienceGainMultiplier.Value) * experienceGainMultiplierIndividual.Value));
}
// apply money gains afterwards to prevent them from affecting XP gains
@@ -436,7 +443,7 @@ namespace Barotrauma
{
if (reputationReward.Key == "location")
{
Locations[0].Reputation?.AddReputation(reputationReward.Value);
OriginLocation.Reputation?.AddReputation(reputationReward.Value);
}
else
{
@@ -546,17 +553,14 @@ namespace Barotrauma
return humanPrefab;
}
protected Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient, bool giveTags = true)
protected static Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.ServerAndClient)
{
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
characterInfo.TeamID = teamType;
if (positionToStayIn == null)
{
positionToStayIn =
positionToStayIn ??=
WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.HumanPrefab = humanPrefab;
@@ -201,7 +201,7 @@ namespace Barotrauma
{
continue;
}
if (Level.Loaded.ExtraWalls.Any(w => w.IsPointInside(position.Position.ToVector2())))
if (Level.Loaded.IsPositionInsideWall(position.Position.ToVector2()))
{
removals.Add(position);
}
@@ -371,7 +371,7 @@ namespace Barotrauma
if (!SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.VanillaContent == null || ContentPackageManager.EnabledPackages.All.Any(p => p.HasMultiplayerSyncedContent && p != GameMain.VanillaContent))
if (ContentPackageManager.ModsEnabled)
{
message = "[MODDED] " + message;
}
@@ -290,6 +290,16 @@ namespace Barotrauma
else if (!character.Info.StartItemsGiven)
{
character.GiveJobItems(mainSubWaypoints[i]);
foreach (Item item in character.Inventory.AllItems)
{
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.SubmarineSpecificID = 0;
}
}
}
if (character.Info.HealthData != null)
{
@@ -298,6 +308,7 @@ namespace Barotrauma
character.LoadTalents();
character.GiveIdCardTags(mainSubWaypoints[i]);
character.GiveIdCardTags(spawnWaypoints[i]);
character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null)
@@ -14,8 +14,9 @@ namespace Barotrauma
{
}
public CampaignMetadata(XElement element)
public void Load(XElement element)
{
data.Clear();
foreach (var subElement in element.Elements())
{
if (string.Equals(subElement.Name.ToString(), "data", StringComparison.InvariantCultureIgnoreCase))
@@ -27,7 +27,7 @@ namespace Barotrauma
/// Get what kind of affiliation this faction has towards the player depending on who they chose to side with via talents
/// </summary>
/// <returns></returns>
public static FactionAffiliation GetPlayerAffiliationStatus(Identifier identifier, ImmutableHashSet<Character>? characterList = null)
public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet<Character>? characterList = null)
{
if (GameMain.GameSession?.Campaign?.Factions is not { } factions) { return FactionAffiliation.Neutral; }
@@ -37,23 +37,20 @@ namespace Barotrauma
{
if (character.Info is not { } info) { continue; }
foreach (Faction faction in factions)
foreach (Faction otherFaction in factions)
{
Identifier factionIdentifier = faction.Prefab.Identifier;
Identifier factionIdentifier = otherFaction.Prefab.Identifier;
if (info.GetSavedStatValue(StatTypes.Affiliation, factionIdentifier) > 0f)
{
return factionIdentifier == identifier
return factionIdentifier == faction.Prefab.Identifier
? FactionAffiliation.Positive
: FactionAffiliation.Negative;
}
}
}
return FactionAffiliation.Neutral;
}
public static FactionAffiliation GetPlayerAffiliationStatus(Faction faction, ImmutableHashSet<Character>? characterList = null) => GetPlayerAffiliationStatus(faction.Prefab.Identifier, characterList);
public override string ToString()
{
return $"{base.ToString()} ({Prefab?.Identifier.ToString() ?? "null"})";
@@ -46,7 +46,7 @@ namespace Barotrauma
private List<Faction> factions;
public IReadOnlyList<Faction> Factions => factions;
public CampaignMetadata CampaignMetadata;
public readonly CampaignMetadata CampaignMetadata;
protected XElement petsElement;
@@ -157,6 +157,7 @@ namespace Barotrauma
CargoManager = new CargoManager(this);
MedicalClinic = new MedicalClinic(this);
CampaignMetadata = new CampaignMetadata();
Identifier messageIdentifier = new Identifier("money");
#if CLIENT
@@ -675,9 +676,11 @@ namespace Barotrauma
//TODO: ignore players who don't have the permission to trigger a transition between levels?
var leavingPlayers = Character.CharacterList.Where(c => !c.IsDead && (c == Character.Controlled || c.IsRemotePlayer));
CharacterTeamType submarineTeam = leavingPlayers.FirstOrDefault()?.TeamID ?? CharacterTeamType.Team1;
//allow leaving if inside an outpost, and the submarine is either docked to it or close enough
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers);
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers);
Submarine leavingSubAtStart = GetLeavingSubAtStart(leavingPlayers, submarineTeam);
Submarine leavingSubAtEnd = GetLeavingSubAtEnd(leavingPlayers, submarineTeam);
int playersInSubAtStart = leavingSubAtStart == null || !leavingSubAtStart.AtStartExit ? 0 :
leavingPlayers.Count(c => c.Submarine == leavingSubAtStart || leavingSubAtStart.DockedTo.Contains(c.Submarine) || (Level.Loaded.StartOutpost != null && c.Submarine == Level.Loaded.StartOutpost));
@@ -691,11 +694,11 @@ namespace Barotrauma
return playersInSubAtStart > playersInSubAtEnd ? leavingSubAtStart : leavingSubAtEnd;
static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers)
static Submarine GetLeavingSubAtStart(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
{
if (Level.Loaded.StartOutpost == null)
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
@@ -705,23 +708,23 @@ namespace Barotrauma
if (Level.Loaded.StartOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; }
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.StartOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
Submarine closestSub = Submarine.FindClosest(Level.Loaded.StartOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null || !closestSub.AtStartExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
}
static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers)
static Submarine GetLeavingSubAtEnd(IEnumerable<Character> leavingPlayers, CharacterTeamType submarineTeam)
{
if (Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.ExitPoints.Any())
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null || !closestSub.AtEndExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
@@ -733,7 +736,7 @@ namespace Barotrauma
if (Level.Loaded.EndOutpost == null)
{
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndExitPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
@@ -743,13 +746,13 @@ namespace Barotrauma
if (Level.Loaded.EndOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != leavingPlayers.FirstOrDefault()?.TeamID) { return null; }
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection && !leavingPlayers.Any(s => s.Submarine == Level.Loaded.EndOutpost)) { return null; }
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: leavingPlayers.FirstOrDefault()?.TeamID);
Submarine closestSub = Submarine.FindClosest(Level.Loaded.EndOutpost.WorldPosition, ignoreOutposts: true, ignoreRespawnShuttle: true, teamType: submarineTeam);
if (closestSub == null || !closestSub.AtEndExit) { return null; }
return closestSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : closestSub;
}
@@ -870,7 +873,7 @@ namespace Barotrauma
}
foreach (Location location in Map.Locations)
{
location.LevelData = new LevelData(location, location.Biome.AdjustedMaxDifficulty);
location.LevelData = new LevelData(location, Map, location.Biome.AdjustedMaxDifficulty);
location.Reset(this);
}
Map.ClearLocationHistory();
@@ -82,7 +82,7 @@ namespace Barotrauma
}
public const int DefaultMaxMissionCount = 2;
public const int MaxMissionCountLimit = 10;
public const int MaxMissionCountLimit = 3;
public const int MinMissionCountLimit = 1;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
@@ -1,4 +1,5 @@
using Barotrauma.IO;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -53,7 +54,7 @@ namespace Barotrauma
}
}
private bool ValidateFlag(NetFlags flag)
private static bool ValidateFlag(NetFlags flag)
{
if (MathHelper.IsPowerOfTwo((int)flag)) { return true; }
#if DEBUG
@@ -105,7 +106,6 @@ namespace Barotrauma
#endif
}
CampaignID = currentCampaignID;
CampaignMetadata = new CampaignMetadata();
UpgradeManager = new UpgradeManager(this);
InitFactions();
}
@@ -194,7 +194,7 @@ namespace Barotrauma
}
break;
case "metadata":
CampaignMetadata = new CampaignMetadata(subElement);
CampaignMetadata.Load(subElement);
break;
case "upgrademanager":
case "pendingupgrades":
@@ -237,10 +237,8 @@ namespace Barotrauma
};
}
CampaignMetadata ??= new CampaignMetadata();
UpgradeManager ??= new UpgradeManager(this);
InitFactions();
#if SERVER
characterData.Clear();
string characterDataPath = GetCharacterDataSavePath();
@@ -179,14 +179,41 @@ namespace Barotrauma
int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
int currentLevel = GetUpgradeLevel(prefab, category);
int newLevel = currentLevel + 1;
int maxLevel = prefab.GetMaxLevelForCurrentSub();
if (currentLevel + 1 > maxLevel)
{
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {maxLevel}). The transaction has been cancelled.");
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled.");
return;
}
bool TryTakeResources(Character character)
{
bool result = prefab.TryTakeResources(character, newLevel);
if (!result)
{
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" but the player does not have the required resources.");
}
return result;
}
switch (GameMain.NetworkMember)
{
case null when Character.Controlled is { } controlled: // singleplayer
if (!TryTakeResources(controlled)) { return; }
break;
case { IsClient: true }:
if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; }
break;
case { IsServer: true } when client?.Character is { } character:
if (!TryTakeResources(character)) { return; }
break;
default:
DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player.");
return;
}
if (price < 0)
{
Location? location = Campaign.Map?.CurrentLocation;
@@ -188,16 +188,26 @@ namespace Barotrauma.Items.Components
private DockingPort FindAdjacentPort()
{
float closestDist = float.MaxValue;
DockingPort closestPort = null;
foreach (DockingPort port in list)
{
if (port == this || port.item.Submarine == item.Submarine || port.IsHorizontal != IsHorizontal) { continue; }
if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) { continue; }
if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) { continue; }
float xDist = Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X);
if (xDist > DistanceTolerance.X) { continue; }
float yDist = Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y);
if (yDist > DistanceTolerance.Y) { continue; }
return port;
float dist = xDist + yDist;
//disfavor non-interactable ports
if (port.item.NonInteractable) { dist *= 2; }
if (dist < closestDist)
{
closestPort = port;
closestDist = dist;
}
}
return null;
return closestPort;
}
private void AttemptDock()
@@ -365,7 +365,8 @@ namespace Barotrauma.Items.Components
{
lastBrokenTime = Timing.TotalTime;
//the door has to be restored to 50% health before collision detection on the body is re-enabled
if (item.ConditionPercentage > 50.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
if (item.ConditionPercentage / Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
IsBroken = false;
}
@@ -480,11 +481,16 @@ namespace Barotrauma.Items.Components
ce = ce.Next;
}
}
if (OutsideSubmarineFixture != null)
{
OutsideSubmarineFixture.CollidesWith = Category.None;
}
linkedGap.Open = 1.0f;
if (linkedGap != null)
{
linkedGap.Open = 1.0f;
}
IsOpen = false;
#if CLIENT
if (convexHull != null) { convexHull.Enabled = false; }
@@ -366,7 +366,6 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < entitiesInRange.Count; i++)
{
float dist = float.MaxValue;
if (entitiesInRange[i] is Structure structure)
{
if (structure.IsHorizontal)
@@ -388,7 +387,7 @@ namespace Barotrauma.Items.Components
}
else if (entitiesInRange[i] is Character character)
{
dist = MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition);
dist = MathF.Sqrt(MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition));
}
if (dist < closestDist)
@@ -49,6 +49,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Disable to make the weapon ignore all hit effects when it collides with walls, doors, or other items.")]
public bool HitOnlyCharacters
{
get;
set;
}
[Editable, Serialize(true, IsPropertySaveable.No)]
public bool Swing { get; set; }
@@ -112,7 +119,7 @@ namespace Barotrauma.Items.Components
reloadTimer = reload;
reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed);
reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier);
character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer * 0.9f;
character.AnimController.LockFlipping();
item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile;
item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking;
@@ -219,7 +226,7 @@ namespace Barotrauma.Items.Components
ac.HoldItem(deltaTime, item, handlePos, aimPos + swingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
if (ac.InWater)
{
ac.LockFlippingUntil = (float)Timing.TotalTime + Reload;
ac.LockFlipping();
}
}
else
@@ -343,33 +350,36 @@ namespace Barotrauma.Items.Components
}
hitTargets.Add(targetCharacter);
}
else if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
else if (!HitOnlyCharacters)
{
if (AllowHitMultiple)
if ((f2.Body.UserData as Structure ?? f2.UserData as Structure) is Structure targetStructure)
{
if (hitTargets.Contains(targetStructure)) { return true; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetStructure)) { return true; }
}
else
{
if (hitTargets.Any(t => t is Structure)) { return true; }
}
hitTargets.Add(targetStructure);
}
else
else if (f2.Body.UserData is Item targetItem)
{
if (hitTargets.Any(t => t is Structure)) { return true; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetItem)) { return true; }
}
else
{
if (hitTargets.Any(t => t is Item)) { return true; }
}
hitTargets.Add(targetItem);
}
hitTargets.Add(targetStructure);
}
else if (f2.Body.UserData is Item targetItem)
{
if (AllowHitMultiple)
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
{
if (hitTargets.Contains(targetItem)) { return true; }
hitTargets.Add(holdable.Item);
}
else
{
if (hitTargets.Any(t => t is Item)) { return true; }
}
hitTargets.Add(targetItem);
}
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
{
hitTargets.Add(holdable.Item);
}
else
{
@@ -381,6 +391,7 @@ namespace Barotrauma.Items.Components
return true;
}
private System.Text.StringBuilder serverLogger;
private void HandleImpact(Fixture targetFixture)
{
var target = targetFixture.Body;
@@ -398,11 +409,13 @@ namespace Barotrauma.Items.Components
Character user = User;
Limb targetLimb = target.UserData as Limb;
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
Item targetItem = target.UserData as Item;
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
if (Attack != null)
{
Attack.SetUser(user);
Attack.DamageMultiplier = damageMultiplier;
if (targetLimb != null)
{
if (targetLimb.character.Removed) { return; }
@@ -415,12 +428,12 @@ namespace Barotrauma.Items.Components
targetCharacter.LastDamageSource = item;
Attack.DoDamage(user, targetCharacter, item.WorldPosition, 1.0f);
}
else if ((target.UserData as Structure ?? targetFixture.UserData as Structure) is Structure targetStructure)
else if (targetStructure != null)
{
if (targetStructure.Removed) { return; }
Attack.DoDamage(user, targetStructure, item.WorldPosition, 1.0f);
}
else if (target.UserData is Item targetItem && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
else if (targetItem != null && targetItem.Prefab.DamagedByMeleeWeapons && targetItem.Condition > 0)
{
if (targetItem.Removed) { return; }
var attackResult = Attack.DoDamage(user, targetItem, item.WorldPosition, 1.0f);
@@ -457,27 +470,43 @@ namespace Barotrauma.Items.Components
{
conditionalActionType = ActionType.OnFailure;
}
if (GameMain.NetworkMember is { IsServer: true } server && targetCharacter != null)
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb));
#if SERVER
if (GameMain.Server != null) //TODO: Log structure hits
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, targetEntity));
serverLogger ??= new System.Text.StringBuilder();
serverLogger.Clear();
serverLogger.Append($"{picker?.LogName} used {item.Name}");
if (item.ContainedItems != null && item.ContainedItems.Any())
{
string logStr = picker?.LogName + " used " + item.Name;
if (item.ContainedItems != null && item.ContainedItems.Any())
{
logStr += " (" + string.Join(", ", item.ContainedItems.Select(i => i?.Name)) + ")";
}
logStr += " on " + targetCharacter.LogName + ".";
Networking.GameServer.Log(logStr, Networking.ServerLog.MessageType.Attack);
serverLogger.Append($"({string.Join(", ", item.ContainedItems.Select(i => i?.Name))})");
}
#endif
string targetName;
if (targetCharacter != null)
{
targetName = targetCharacter.LogName;
}
else if (targetItem != null)
{
targetName = targetItem.Name;
}
else if (targetStructure != null)
{
targetName = targetStructure.Name;
}
else
{
targetName = targetEntity.ToString();
}
serverLogger.Append($" on {targetName}.");
#if SERVER
Networking.GameServer.Log(serverLogger.ToString(), Networking.ServerLog.MessageType.Attack);
#endif
}
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
if (targetEntity != null)
{
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
}
if (DeleteOnUse)
@@ -215,6 +215,8 @@ namespace Barotrauma.Items.Components
baseReloadTime = MathHelper.Lerp(reload, ReloadNoSkill, reloadFailure);
}
ReloadTimer = baseReloadTime / (1 + character?.GetStatValue(StatTypes.RangedAttackSpeed) ?? 0f);
ReloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.FiringRateMultiplier);
currentChargeTime = 0f;
if (character != null)
@@ -4,7 +4,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.MapCreatures.Behavior;
@@ -185,24 +184,23 @@ namespace Barotrauma.Items.Components
float degreeOfSuccess = character == null ? 0.5f : DegreeOfSuccess(character);
bool failed = false;
if (Rand.Range(0.0f, 0.5f) > degreeOfSuccess)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
failed = true;
}
if (UsableIn == UseEnvironment.None)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
failed = true;
}
if (item.InWater)
{
if (UsableIn == UseEnvironment.Air)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
failed = true;
}
}
else
@@ -210,9 +208,15 @@ namespace Barotrauma.Items.Components
if (UsableIn == UseEnvironment.Water)
{
ApplyStatusEffects(ActionType.OnFailure, deltaTime, character);
return false;
failed = true;
}
}
if (failed)
{
// Always apply ActionType.OnUse. If doesn't fail, the effect is called later.
ApplyStatusEffects(ActionType.OnUse, deltaTime, character);
return false;
}
Vector2 rayStart;
Vector2 rayStartWorld;
@@ -312,9 +316,12 @@ namespace Barotrauma.Items.Components
var collisionCategories = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepair;
//if the item can cut off limbs, activate nearby bodies to allow the raycast to hit them
if (statusEffectLists != null && statusEffectLists.ContainsKey(ActionType.OnUse))
if (statusEffectLists != null)
{
if (statusEffectLists[ActionType.OnUse].Any(s => s.SeverLimbsProbability > 0.0f))
static bool CanSeverJoints(ActionType type, Dictionary<ActionType, List<StatusEffect>> effectList) =>
effectList.TryGetValue(type, out List<StatusEffect> effects) && effects.Any(e => e.SeverLimbsProbability > 0);
if (CanSeverJoints(ActionType.OnUse, statusEffectLists) || CanSeverJoints(ActionType.OnSuccess, statusEffectLists))
{
float rangeSqr = ConvertUnits.ToSimUnits(Range);
rangeSqr *= rangeSqr;
@@ -537,6 +544,7 @@ namespace Barotrauma.Items.Components
if (nonFixableEntities.Contains(targetStructure.Prefab.Identifier) || nonFixableEntities.Any(t => targetStructure.Tags.Contains(t))) { return false; }
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, structure: targetStructure);
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, structure: targetStructure);
FixStructureProjSpecific(user, deltaTime, targetStructure, sectionIndex);
float structureFixAmount = StructureFixAmount;
@@ -605,6 +613,7 @@ namespace Barotrauma.Items.Components
}
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetCharacter, limb: closestLimb);
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetCharacter, limb: closestLimb);
FixCharacterProjSpecific(user, deltaTime, targetCharacter);
return true;
}
@@ -621,6 +630,7 @@ namespace Barotrauma.Items.Components
targetLimb.character.LastDamageSource = item;
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, character: targetLimb.character, limb: targetLimb);
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, character: targetLimb.character, limb: targetLimb);
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
return true;
}
@@ -663,6 +673,7 @@ namespace Barotrauma.Items.Components
targetItem.IsHighlighted = true;
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnUse, targetItem);
ApplyStatusEffectsOnTarget(user, deltaTime, ActionType.OnSuccess, targetItem);
if (targetItem.body != null && !MathUtils.NearlyEqual(TargetForce, 0.0f))
{
@@ -875,7 +886,7 @@ namespace Barotrauma.Items.Components
}
else if (effect.HasTargetType(StatusEffect.TargetType.Character))
{
currentTargets.Add(character);
currentTargets.Add(user);
effect.Apply(actionType, deltaTime, item, currentTargets);
}
else if (effect.HasTargetType(StatusEffect.TargetType.Limb))
@@ -210,7 +210,7 @@ namespace Barotrauma.Items.Components
if (!(GameMain.NetworkMember is { IsClient: true }))
{
//Stun grenades, flares, etc. all have their throw-related things handled in "onSecondaryUse"
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, user: CurrentThrower);
ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, CurrentThrower, useTarget: CurrentThrower, user: CurrentThrower);
}
throwState = ThrowState.None;
}
@@ -849,7 +849,13 @@ namespace Barotrauma.Items.Components
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
if (user != null) { effect.SetUser(user); }
effect.AfflictionMultiplier = afflictionMultiplier;
item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
var c = character;
if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
// A bit hacky, but fixes MeleeWeapons targeting the use target instead of the attacker. Also applies to Projectiles and Throwables, or other callers that passes the user.
c = user;
}
item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
effect.AfflictionMultiplier = 1.0f;
reducesCondition |= effect.ReducesItemCondition();
}
@@ -762,7 +762,6 @@ namespace Barotrauma.Items.Components
var relatedItem = FindContainableItem(contained);
if (relatedItem != null)
{
if (relatedItem.Hide.HasValue && relatedItem.Hide.Value) { continue; }
if (relatedItem.ItemPos.HasValue)
{
Vector2 pos = relatedItem.ItemPos.Value;
@@ -431,7 +431,7 @@ namespace Barotrauma.Items.Components
//disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll
if (character.AnimController is HumanoidAnimController humanoidAnim)
{
humanoidAnim.LockFlippingUntil = (float)Timing.TotalTime + 0.5f;
humanoidAnim.LockFlipping(0.5f);
}
if (character.SelectedItem == item) { character.SelectedItem = null; }
@@ -874,6 +874,6 @@ namespace Barotrauma.Items.Components
}
private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput);
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate);
private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelConsumption, fuelConsumptionRate);
}
}
@@ -781,7 +781,7 @@ namespace Barotrauma.Items.Components
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
return false;
}
if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User))
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
{
return false;
}
@@ -789,7 +789,13 @@ namespace Barotrauma.Items.Components
else if (target.Body.UserData is Item item)
{
if (item.Condition <= 0.0f) { return false; }
if (!item.Prefab.DamagedByProjectiles) { return false; }
if (!item.Prefab.DamagedByProjectiles)
{
if (item.GetComponent<Door>() == null)
{
return false;
}
}
}
else if (target.Body.UserData is Holdable { CanPush: false })
{
@@ -903,7 +909,7 @@ namespace Barotrauma.Items.Components
}
else if (target.Body.UserData is Limb limb)
{
if (!FriendlyFire && User != null && limb.character.IsFriendly(User) && HumanAIController.IsOnFriendlyTeam(limb.character, User))
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
{
return false;
}
@@ -922,6 +928,8 @@ namespace Barotrauma.Items.Components
else if ((target.Body.UserData as Item ?? (target.Body.UserData as ItemComponent)?.Item ?? target.UserData as Item) is Item targetItem)
{
if (targetItem.Removed) { return false; }
//hit the external collider of an item (turret?) of the same sub -> ignore
if (target.UserData is Item && targetItem.Submarine != null && targetItem.Submarine == Launcher?.Submarine) { return false; }
if (Attack != null && (targetItem.Prefab.DamagedByProjectiles || DamageDoors && targetItem.GetComponent<Door>() != null) && targetItem.Condition > 0)
{
attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f);
@@ -974,8 +982,8 @@ namespace Barotrauma.Items.Components
{
if (target.Body.UserData is Limb targetLimb)
{
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: User);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, user: User);
ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: character, user: User);
ApplyStatusEffects(ActionType.OnImpact, 1.0f, character, targetLimb, useTarget: character, user: User);
var attack = targetLimb.attack;
if (attack != null)
{
@@ -986,14 +994,22 @@ namespace Barotrauma.Items.Components
{
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character, targetLimb.WorldPosition);
effect.Apply(effect.type, 1.0f, User, User);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) || effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb.character);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
{
effect.Apply(effect.type, 1.0f, targetLimb.character, targetLimb);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
targets.Clear();
effect.AddNearbyTargets(targetLimb.WorldPosition, targets);
effect.Apply(ActionType.OnActive, 1.0f, targetLimb.character, targets);
effect.Apply(effect.type, 1.0f, targetLimb.character, targets);
}
}
}
@@ -83,7 +83,15 @@ namespace Barotrauma.Items.Components
{
if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; }
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f);
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, level.LevelData.Biome.ActualMaxDifficulty / 100.0f);
if (level.Type == LevelData.LevelType.Outpost &&
level.StartLocation?.Type?.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
//no high-quality spawns in friendly outposts
difficultyFactor = 0.0f;
}
return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync);
static float GetCommonness(int quality, float difficultyFactor)
@@ -159,9 +159,11 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
var user = item.GetComponent<Projectile>()?.User;
if (source == null || target == null || target.Removed ||
(source is Entity sourceEntity && sourceEntity.Removed) ||
(source is Limb limb && limb.Removed))
(source is Limb limb && limb.Removed) ||
(user != null && user.Removed))
{
ResetSource();
target = null;
@@ -293,7 +295,6 @@ namespace Barotrauma.Items.Components
{
targetMass = float.MaxValue;
}
var user = item.GetComponent<Projectile>()?.User;
if (targetMass > TargetMinMass)
{
if (Math.Abs(SourcePullForce) > 0.001f)
@@ -302,7 +303,7 @@ namespace Barotrauma.Items.Components
if (sourceBody != null)
{
var targetBody = GetBodyToPull(target);
if (targetBody != null && !(targetBody.UserData is Character))
if (targetBody != null && targetBody.UserData is not Character)
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
@@ -94,7 +94,7 @@ namespace Barotrauma.Items.Components
if (isOn == value && IsActive == value) { return; }
IsActive = isOn = value;
SetLightSourceState(value, value ? lightBrightness : 0.0f);
SetLightSourceState(value);
OnStateChanged();
}
}
@@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (Light != null)
{
Light.Color = IsOn ? lightColor.Multiply(currentBrightness) : Color.Transparent;
Light.Color = IsOn ? lightColor.Multiply(lightBrightness) : Color.Transparent;
}
#endif
}
@@ -205,7 +205,7 @@ namespace Barotrauma.Items.Components
{
if (base.IsActive == value) { return; }
base.IsActive = isOn = value;
SetLightSourceState(value, value ? lightBrightness : 0.0f);
SetLightSourceState(value);
}
}
@@ -236,7 +236,7 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
SetLightSourceState(IsActive, lightBrightness);
SetLightSourceState(IsActive);
turret = item.GetComponent<Turret>();
#if CLIENT
if (Screen.Selected.IsEditor)
@@ -248,6 +248,12 @@ namespace Barotrauma.Items.Components
public override void OnMapLoaded()
{
#if CLIENT
if (item.HiddenInGame)
{
Light.Enabled = false;
}
#endif
CheckIfNeedsUpdate();
}
@@ -263,8 +269,7 @@ namespace Barotrauma.Items.Components
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
SetLightSourceState(true);
SetLightSourceTransformProjSpecific();
base.IsActive = false;
isOn = true;
@@ -285,13 +290,15 @@ namespace Barotrauma.Items.Components
UpdateAITarget(item.AiTarget);
}
UpdateOnActiveEffects(deltaTime);
//something in UpdateOnActiveEffects may deactivate the light -> return so we don't turn it back on
if (!IsActive) { return; }
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
if (item.Container != null && !(item.GetRootInventoryOwner() is Character))
if (item.Container != null && item.GetRootInventoryOwner() is not Character)
{
SetLightSourceState(false, 0.0f);
SetLightSourceState(false);
return;
}
@@ -300,7 +307,7 @@ namespace Barotrauma.Items.Components
PhysicsBody body = ParentBody ?? item.body;
if (body != null && !body.Enabled)
{
SetLightSourceState(false, 0.0f);
SetLightSourceState(false);
return;
}
@@ -325,7 +332,7 @@ namespace Barotrauma.Items.Components
public override void UpdateBroken(float deltaTime, Camera cam)
{
SetLightSourceState(false, 0.0f);
SetLightSourceState(false);
}
public override bool Use(float deltaTime, Character character = null)
@@ -357,7 +364,7 @@ namespace Barotrauma.Items.Components
{
LightColor = XMLExtensions.ParseColor(signal.value, false);
#if CLIENT
SetLightSourceState(Light.Enabled, currentBrightness);
SetLightSourceState(Light.Enabled);
#endif
prevColorSignal = signal.value;
}
@@ -375,7 +382,7 @@ namespace Barotrauma.Items.Components
target.SightRange = Math.Max(target.SightRange, target.MaxSightRange * lightBrightness);
}
partial void SetLightSourceState(bool enabled, float brightness);
partial void SetLightSourceState(bool enabled, float? brightness = null);
public void SetLightSourceTransform()
{
@@ -478,7 +478,9 @@ namespace Barotrauma.Items.Components
{
// single charged shot guns will decharge after firing
// for cosmetic reasons, this is done by lerping in half the reload time
currentChargeTime = Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f));
currentChargeTime = reloadTime > 0.0f ?
Math.Max(0f, MaxChargeTime * (reload / reloadTime - 0.5f)) :
0.0f;
}
else
{
@@ -268,6 +268,8 @@ namespace Barotrauma
get { return capacity; }
}
public int EmptySlotCount => slots.Count(i => !i.Empty());
public bool AllowSwappingContainedItems = true;
public Inventory(Entity owner, int capacity, int slotsPerRow = 5)
@@ -887,10 +889,7 @@ namespace Barotrauma
}
if (recursive)
{
if (item.OwnInventory != null)
{
item.OwnInventory.FindAllItems(predicate, recursive: true, list);
}
item.OwnInventory?.FindAllItems(predicate, recursive: true, list);
}
}
return list;
@@ -111,6 +111,7 @@ namespace Barotrauma
private float sendConditionUpdateTimer;
private bool conditionUpdatePending;
private float prevCondition;
private float condition;
private bool inWater;
@@ -1584,7 +1585,7 @@ namespace Barotrauma
tags.Add(newTag);
}
public IEnumerable<Identifier> GetTags()
public IReadOnlyCollection<Identifier> GetTags()
{
return tags;
}
@@ -1753,15 +1754,14 @@ namespace Barotrauma
if (Indestructible) { return; }
if (InvulnerableToDamage && value <= condition) { return; }
float prev = condition;
bool wasInFullCondition = IsFullCondition;
condition = MathHelper.Clamp(value, 0.0f, MaxCondition);
if (MathUtils.NearlyEqual(prev, condition, epsilon: 0.000001f)) { return; }
if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; }
RecalculateConditionValues();
if (condition == 0.0f && prev > 0.0f)
if (condition == 0.0f && prevCondition > 0.0f)
{
//Flag connections to be updated as device is broken
flagChangedConnections(connections);
@@ -1775,7 +1775,7 @@ namespace Barotrauma
#endif
ApplyStatusEffects(ActionType.OnBroken, 1.0f, null);
}
else if (condition > 0.0f && prev <= 0.0f)
else if (condition > 0.0f && prevCondition <= 0.0f)
{
//Flag connections to be updated as device is now working again
flagChangedConnections(connections);
@@ -1803,8 +1803,9 @@ namespace Barotrauma
}
}
LastConditionChange = condition - prev;
LastConditionChange = condition - prevCondition;
ConditionLastUpdated = Timing.TotalTime;
prevCondition = condition;
static void flagChangedConnections(Dictionary<string, Connection> connections)
{
@@ -2172,8 +2173,9 @@ namespace Barotrauma
var projectile = GetComponent<Projectile>();
if (projectile != null)
{
//ignore character colliders (a projectile only hits limbs)
if (f2.CollisionCategories == Physics.CollisionCharacter && f2.Body.UserData is Character) { return false; }
// Ignore characters so that the impact sound only plays when the item hits a a wall or a door.
// Projectile collisions are handled in Projectile.OnProjectileCollision(), so it should be safe to do this.
if (f2.CollisionCategories == Physics.CollisionCharacter) { return false; }
if (projectile.IgnoredBodies != null && projectile.IgnoredBodies.Contains(f2.Body)) { return false; }
if (projectile.ShouldIgnoreSubmarineCollision(f2, contact)) { return false; }
}
@@ -2726,7 +2728,7 @@ namespace Barotrauma
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb);
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: targetLimb?.character, user: character);
if (ic.DeleteOnUse) { remove = true; }
}
@@ -2757,7 +2759,7 @@ namespace Barotrauma
#if CLIENT
ic.PlaySound(ActionType.OnSecondaryUse, character);
#endif
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character);
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character);
if (ic.DeleteOnUse) { remove = true; }
}
@@ -2796,8 +2798,8 @@ namespace Barotrauma
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, user: user);
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, user: user);
ic.ApplyStatusEffects(conditionalActionType, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user);
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, character, targetLimb, useTarget: targetLimb?.character, user: user);
if (GameMain.NetworkMember is { IsServer: true })
{
@@ -3463,7 +3465,7 @@ namespace Barotrauma
item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition);
}
}
item.lastSentCondition = item.condition;
item.lastSentCondition = item.prevCondition = item.condition;
item.RecalculateConditionValues();
item.SetActiveSprite();
@@ -3537,15 +3539,10 @@ namespace Barotrauma
upgrade.Save(element);
}
if (condition < MaxCondition)
{
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
}
else
{
var conditionAttribute = element.GetAttribute("condition");
if (conditionAttribute != null) { conditionAttribute.Remove(); }
}
element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture)));
var conditionAttribute = element.GetAttribute("condition");
if (conditionAttribute != null) { conditionAttribute.Remove(); }
parentElement.Add(element);
@@ -30,7 +30,7 @@ namespace Barotrauma
public EventType EventType { get; }
}
public struct ComponentStateEventData : IEventData
public readonly struct ComponentStateEventData : IEventData
{
public EventType EventType => EventType.ComponentState;
public readonly ItemComponent Component;
@@ -867,6 +867,7 @@ namespace Barotrauma
(endPath != null && GetDistToTunnel(cell.Center, endPath) < minMainPathWidth) ||
(endHole != null && GetDistToTunnel(cell.Center, endHole) < minMainPathWidth)) { continue; }
if (cell.Edges.Any(e => e.AdjacentCell(cell)?.CellType != CellType.Path || e.NextToCave)) { continue; }
if (PositionsOfInterest.Any(p => cell.IsPointInside(p.Position.ToVector2()))) { continue; }
potentialIslands.Add(cell);
}
for (int i = 0; i < GenerationParams.IslandCount; i++)
@@ -1112,6 +1113,7 @@ namespace Barotrauma
caveCells.AddRange(cave.Tunnels.SelectMany(t => t.Cells));
foreach (var caveCell in caveCells)
{
if (PositionsOfInterest.Any(p => caveCell.IsPointInside(p.Position.ToVector2()))) { continue; }
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < destructibleWallRatio * cave.CaveGenerationParams.DestructibleWallRatio)
{
var chunk = CreateIceChunk(caveCell.Edges, caveCell.Center, health: 50.0f);
@@ -3189,7 +3191,7 @@ namespace Barotrauma
TryGetInterestingPosition(true, spawnPosType, minDistFromSubs, out Vector2 startPos, filter);
Vector2 offset = Rand.Vector(Rand.Range(0.0f, randomSpread, Rand.RandSync.ServerAndClient), Rand.RandSync.ServerAndClient);
if (!cells.Any(c => c.IsPointInside(startPos + offset)))
if (!IsPositionInsideWall(startPos + offset))
{
startPos += offset;
}
@@ -3245,10 +3247,9 @@ namespace Barotrauma
{
suitablePositions.RemoveAll(p => !filter(p));
}
//avoid floating ice chunks on the main path
if (positionType.HasFlag(PositionType.MainPath) || positionType.HasFlag(PositionType.SidePath))
{
suitablePositions.RemoveAll(p => ExtraWalls.Any(w => w.Cells.Any(c => c.IsPointInside(p.Position.ToVector2()))));
suitablePositions.RemoveAll(p => IsPositionInsideWall(p.Position.ToVector2()));
}
if (!suitablePositions.Any())
{
@@ -3301,10 +3302,16 @@ namespace Barotrauma
return false;
}
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, (useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced))].Position;
position = farEnoughPositions[Rand.Int(farEnoughPositions.Count, useSyncedRand ? Rand.RandSync.ServerAndClient : Rand.RandSync.Unsynced)].Position;
return true;
}
public bool IsPositionInsideWall(Vector2 worldPosition)
{
var closestCell = GetClosestCell(worldPosition);
return closestCell != null && closestCell.IsPointInside(worldPosition);
}
public void Update(float deltaTime, Camera cam)
{
LevelObjectManager.Update(deltaTime);
@@ -3347,14 +3354,13 @@ namespace Barotrauma
public Vector2 GetBottomPosition(float xPosition)
{
int index = (int)Math.Floor(xPosition / Size.X * (bottomPositions.Count - 1));
float interval = Size.X / (bottomPositions.Count - 1);
int index = (int)Math.Floor(xPosition / interval);
if (index < 0 || index >= bottomPositions.Count - 1) { return new Vector2(xPosition, BottomPos); }
float t = (xPosition - bottomPositions[index].X) / (bottomPositions[index + 1].X - bottomPositions[index].X);
//t can go slightly outside the 0-1 due to rounding, safe to ignore
Debug.Assert(t <= 1.001f && t >= -0.001f);
float t = (xPosition - bottomPositions[index].X) / interval;
t = MathHelper.Clamp(t, 0.0f, 1.0f);
float yPos = MathHelper.Lerp(bottomPositions[index].Y, bottomPositions[index + 1].Y, t);
return new Vector2(xPosition, yPos);
@@ -161,7 +161,7 @@ namespace Barotrauma
/// </summary>
public LevelData(LocationConnection locationConnection)
{
Seed = locationConnection.Locations[0].BaseName + locationConnection.Locations[1].BaseName;
Seed = locationConnection.Locations[0].LevelData.Seed + locationConnection.Locations[1].LevelData.Seed;
Biome = locationConnection.Biome;
Type = LevelType.LocationConnection;
Difficulty = locationConnection.Difficulty;
@@ -194,9 +194,9 @@ namespace Barotrauma
/// <summary>
/// Instantiates level data using the properties of the location
/// </summary>
public LevelData(Location location, float difficulty)
public LevelData(Location location, Map map, float difficulty)
{
Seed = location.BaseName;
Seed = location.BaseName + map.Locations.IndexOf(location);
Biome = location.Biome;
Type = LevelType.Outpost;
Difficulty = difficulty;
@@ -646,7 +646,7 @@ namespace Barotrauma
}
}
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biome = default)
public static LevelGenerationParams GetRandom(string seed, LevelData.LevelType type, float difficulty, Identifier biomeId = default)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
@@ -661,14 +661,29 @@ namespace Barotrauma
lp.Type == type &&
(lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Any()) &&
!lp.AllowedBiomeIdentifiers.Contains("None".ToIdentifier()));
matchingLevelParams = biome.IsEmpty
? matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome))
: matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biome));
if (biomeId.IsEmpty)
{
//we don't want end levels when generating a completely random level (e.g. in mission mode)
matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || !lp.AllowedBiomeIdentifiers.All(b => Biome.Prefabs[b].IsEndBiome));
}
else
{
bool isEndBiome = Biome.Prefabs.TryGet(biomeId, out Biome biome) && biome.IsEndBiome;
if (isEndBiome && matchingLevelParams.Any(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId)))
{
//in the end biome, we must choose level parameters meant specifically for the end levels
matchingLevelParams = matchingLevelParams.Where(lp => lp.AllowedBiomeIdentifiers.Contains(biomeId));
}
else
{
matchingLevelParams = matchingLevelParams.Where(lp => lp.AnyBiomeAllowed || lp.AllowedBiomeIdentifiers.Contains(biomeId));
}
}
if (!matchingLevelParams.Any())
{
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")");
if (!biome.IsEmpty)
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\")");
if (!biomeId.IsEmpty)
{
//try to find params that at least have a suitable type
matchingLevelParams = levelParamsOrdered.Where(lp => lp.Type == type);
@@ -682,7 +697,7 @@ namespace Barotrauma
if (!matchingLevelParams.Any(lp => difficulty >= lp.MinLevelDifficulty && difficulty <= lp.MaxLevelDifficulty))
{
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biome.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})");
DebugConsole.ThrowError($"Suitable level generation presets not found (biome \"{biomeId.IfEmpty("null".ToIdentifier())}\", type: \"{type}\", difficulty: {difficulty})");
}
else
{
@@ -457,12 +457,14 @@ namespace Barotrauma
private struct LoadedMission
{
public MissionPrefab MissionPrefab { get; }
public int OriginLocationIndex { get; }
public int DestinationIndex { get; }
public bool SelectedMission { get; }
public LoadedMission(MissionPrefab prefab, int destinationIndex, bool selectedMission)
public LoadedMission(MissionPrefab prefab, int originLocationIndex, int destinationIndex, bool selectedMission)
{
MissionPrefab = prefab;
OriginLocationIndex = originLocationIndex;
DestinationIndex = destinationIndex;
SelectedMission = selectedMission;
}
@@ -663,9 +665,10 @@ namespace Barotrauma
if (string.IsNullOrWhiteSpace(id)) { continue; }
var prefab = MissionPrefab.Prefabs.Find(p => p.Identifier == id);
if (prefab == null) { continue; }
var origin = childElement.GetAttributeInt("origin", -1);
var destination = childElement.GetAttributeInt("destinationindex", -1);
var selected = childElement.GetAttributeBool("selected", false);
loadedMissions.Add(new LoadedMission(prefab, destination, selected));
loadedMissions.Add(new LoadedMission(prefab, origin, destination, selected));
}
}
}
@@ -926,6 +929,10 @@ namespace Barotrauma
destination = Connections.First().OtherLocation(this);
}
var mission = loadedMission.MissionPrefab.Instantiate(new Location[] { this, destination }, Submarine.MainSub);
if (loadedMission.OriginLocationIndex >= 0 && loadedMission.OriginLocationIndex < map.Locations.Count)
{
mission.OriginLocation = map.Locations[loadedMission.OriginLocationIndex];
}
availableMissions.Add(mission);
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
}
@@ -1520,10 +1527,12 @@ namespace Barotrauma
foreach (Mission mission in missions)
{
var location = mission.Locations.All(l => l == this) ? this : mission.Locations.FirstOrDefault(l => l != this);
var i = map.Locations.IndexOf(location);
var destinationIndex = map.Locations.IndexOf(location);
var originIndex = map.Locations.IndexOf(mission.OriginLocation);
missionsElement.Add(new XElement("mission",
new XAttribute("prefabid", mission.Prefab.Identifier),
new XAttribute("destinationindex", i),
new XAttribute("destinationindex", destinationIndex),
new XAttribute("origin", originIndex),
new XAttribute("selected", selectedMissions.Contains(mission))));
}
locationElement.Add(missionsElement);
@@ -167,7 +167,7 @@ namespace Barotrauma
}
int startLocationindex = element.GetAttributeInt("startlocation", -1);
if (startLocationindex > 0 && startLocationindex < Locations.Count)
if (startLocationindex >= 0 && startLocationindex < Locations.Count)
{
StartLocation = Locations[startLocationindex];
}
@@ -188,10 +188,9 @@ namespace Barotrauma
{
//backwards compatibility
int endLocationIndex = element.GetAttributeInt("endlocation", -1);
if (endLocationIndex > 0 && endLocationIndex < Locations.Count)
if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
{
endLocations.Add(Locations[endLocationIndex]);
Locations[endLocationIndex].LevelData.ReassignGenerationParams(Seed);
}
else
{
@@ -203,7 +202,7 @@ namespace Barotrauma
int[] endLocationindices = element.GetAttributeIntArray("endlocations", Array.Empty<int>());
foreach (int endLocationIndex in endLocationindices)
{
if (endLocationIndex > 0 && endLocationIndex < Locations.Count)
if (endLocationIndex >= 0 && endLocationIndex < Locations.Count)
{
endLocations.Add(Locations[endLocationIndex]);
}
@@ -245,7 +244,7 @@ namespace Barotrauma
{
Biome = endLocations.First().Biome
};
newEndLocation.LevelData = new LevelData(newEndLocation, difficulty: 100.0f);
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
Locations.Add(newEndLocation);
endLocations.Add(newEndLocation);
}
@@ -339,7 +338,7 @@ namespace Barotrauma
{
if (StartLocation != null)
{
StartLocation.LevelData = new LevelData(StartLocation, 0);
StartLocation.LevelData = new LevelData(StartLocation, this, 0);
}
//ensure all paths from the starting location have 0 difficulty to make the 1st campaign round very easy
@@ -701,7 +700,7 @@ namespace Barotrauma
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location, CalculateDifficulty(location.MapPosition.X, location.Biome));
location.LevelData = new LevelData(location, this, CalculateDifficulty(location.MapPosition.X, location.Biome));
if (location.Type.HasOutpost && campaign != null && location.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
location.Faction ??= campaign.GetRandomFaction(Rand.RandSync.ServerAndClient);
@@ -902,6 +901,7 @@ namespace Barotrauma
{
for (int i = 0; i < endLocations.Count; i++)
{
endLocations[i].LevelData.ReassignGenerationParams(Seed);
var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
if (outpostParams != null)
{
@@ -124,9 +124,18 @@ namespace Barotrauma
int eventCount = GameMain.Server.EntityEventManager.Events.Count();
int uniqueEventCount = GameMain.Server.EntityEventManager.UniqueEvents.Count();
#endif
List<MapEntity> entities = MapEntity.mapEntityList.FindAll(e => e.Submarine == sub);
HashSet<Submarine> connectedSubs = new HashSet<Submarine>() { sub };
foreach (Submarine otherSub in Submarine.Loaded)
{
//remove linked subs too
if (otherSub.Submarine == sub) { connectedSubs.Add(otherSub); }
}
List<MapEntity> entities = MapEntity.mapEntityList.FindAll(e => connectedSubs.Contains(e.Submarine));
entities.ForEach(e => e.Remove());
sub.Remove();
foreach (Submarine otherSub in connectedSubs)
{
otherSub.Remove();
}
#if SERVER
//remove any events created during the removal of the entities
GameMain.Server.EntityEventManager.Events.RemoveRange(eventCount, GameMain.Server.EntityEventManager.Events.Count - eventCount);
@@ -543,12 +552,8 @@ namespace Barotrauma
foreach (OutpostModuleInfo.GapPosition gapPosition in GapPositions.Randomize(Rand.RandSync.ServerAndClient))
{
if (currentModule.UsedGapPositions.HasFlag(gapPosition)) { continue; }
if (!allowExtendBelowInitialModule)
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { continue; }
}
if (DisallowBelowAirlock(allowExtendBelowInitialModule, gapPosition, currentModule)) { continue; }
PlacedModule newModule = null;
//try appending to the current module if possible
if (currentModule.Info.OutpostModuleInfo.GapPositions.HasFlag(gapPosition))
@@ -569,6 +574,7 @@ namespace Barotrauma
foreach (OutpostModuleInfo.GapPosition otherGapPosition in
GapPositions.Where(g => !otherModule.UsedGapPositions.HasFlag(g) && otherModule.Info.OutpostModuleInfo.GapPositions.HasFlag(g)))
{
if (DisallowBelowAirlock(allowExtendBelowInitialModule, otherGapPosition, otherModule)) { continue; }
newModule = AppendModule(otherModule, GetOpposingGapPosition(otherGapPosition), availableModules, pendingModuleFlags, selectedModules, locationType, allowDifferentLocationType);
if (newModule != null)
{
@@ -617,6 +623,16 @@ namespace Barotrauma
{
System.Diagnostics.Debug.Assert(selectedModules.All(m => m.PreviousModule == null || selectedModules.Contains(m.PreviousModule)));
}
static bool DisallowBelowAirlock(bool allowExtendBelowInitialModule, OutpostModuleInfo.GapPosition gapPosition, PlacedModule currentModule)
{
if (!allowExtendBelowInitialModule)
{
//don't continue downwards if it'd extend below the airlock
if (gapPosition == OutpostModuleInfo.GapPosition.Bottom && currentModule.Offset.Y <= 1) { return true; }
}
return false;
}
}
/// <summary>
@@ -1367,7 +1367,7 @@ namespace Barotrauma
return new Rectangle((int)bounds.X, (int)bounds.Y, (int)(bounds.Z - bounds.X), (int)(bounds.Y - bounds.W));
}
public Submarine(SubmarineInfo info, bool showWarningMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
{
upgradeEventIdentifier = new Identifier($"Submarine{ID}");
Loading = true;
@@ -1438,64 +1438,65 @@ namespace Barotrauma
center.Y -= center.Y % GridSize.Y;
RepositionEntities(-center, MapEntity.mapEntityList.Where(me => me.Submarine == this));
}
subBody = new SubmarineBody(this, showWarningMessages);
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
subBody = new SubmarineBody(this, showErrorMessages);
Vector2 pos = ConvertUnits.ToSimUnits(HiddenSubPosition);
subBody.Body.FarseerBody.SetTransformIgnoreContacts(ref pos, 0.0f);
if (info.IsOutpost)
if (info.IsOutpost)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
TeamID = CharacterTeamType.FriendlyNPC;
bool indestructible =
GameMain.NetworkMember != null &&
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
foreach (MapEntity me in MapEntity.mapEntityList)
{
ShowSonarMarker = false;
TeamID = CharacterTeamType.FriendlyNPC;
bool indestructible =
GameMain.NetworkMember != null &&
!GameMain.NetworkMember.ServerSettings.DestructibleOutposts &&
!(info.OutpostGenerationParams?.AlwaysDestructible ?? false);
foreach (MapEntity me in MapEntity.mapEntityList)
if (me.Submarine != this) { continue; }
if (me is Item item)
{
if (me.Submarine != this) { continue; }
if (me is Item item)
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
if (item.GetComponent<Repairable>() != null && indestructible)
{
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
if (item.GetComponent<Repairable>() != null && indestructible)
item.Indestructible = true;
}
foreach (ItemComponent ic in item.Components)
{
if (ic is ConnectionPanel connectionPanel)
{
item.Indestructible = true;
}
foreach (ItemComponent ic in item.Components)
{
if (ic is ConnectionPanel connectionPanel)
//prevent rewiring
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
{
//prevent rewiring
if (info.OutpostGenerationParams != null && !info.OutpostGenerationParams.AlwaysRewireable)
{
connectionPanel.Locked = true;
}
connectionPanel.Locked = true;
}
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
{
//prevent deattaching items from walls
}
else if (ic is Holdable holdable && holdable.Attached && item.GetComponent<LevelResource>() == null)
{
//prevent deattaching items from walls
#if CLIENT
if (GameMain.GameSession?.GameMode is TutorialMode) { continue; }
#endif
holdable.CanBePicked = false;
holdable.CanBeSelected = false;
}
holdable.CanBePicked = false;
holdable.CanBeSelected = false;
}
}
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
{
structure.Indestructible = true;
}
}
else if (me is Structure structure && structure.Prefab.IndestructibleInOutposts && indestructible)
{
structure.Indestructible = true;
}
}
else if (info.IsRuin)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
}
}
else if (info.IsRuin)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
}
if (entityGrid != null)
@@ -1542,7 +1543,7 @@ namespace Barotrauma
#endif
//if the sub was made using an older version,
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
if (showWarningMessages &&
if (showErrorMessages &&
!string.IsNullOrEmpty(Info.FilePath) &&
Screen.Selected != GameMain.SubEditorScreen &&
(Info.GameVersion == null || Info.GameVersion < new Version("0.8.9.0")))
@@ -116,7 +116,7 @@ namespace Barotrauma
get { return submarine; }
}
public SubmarineBody(Submarine sub, bool showWarningMessages = true)
public SubmarineBody(Submarine sub, bool showErrorMessages = true)
{
this.submarine = sub;
@@ -126,9 +126,9 @@ namespace Barotrauma
if (!Hull.HullList.Any(h => h.Submarine == sub))
{
farseerBody = GameMain.World.CreateRectangle(1.0f, 1.0f, 1.0f);
if (showWarningMessages)
if (showErrorMessages)
{
DebugConsole.ThrowError("WARNING: no hulls found, generating a physics body for the submarine failed.");
DebugConsole.ThrowError($"No hulls found in the submarine \"{sub.Info.Name}\". Generating a physics body for the submarine failed.");
}
}
else
@@ -34,7 +34,7 @@
interface IServerPositionSync : IServerSerializable
{
#if SERVER
void ServerWritePosition(IWriteMessage msg, Client c);
void ServerWritePosition(ReadWriteMessage tempBuffer, Client c);
#endif
#if CLIENT
void ClientReadPosition(IReadMessage msg, float sendingTime);
@@ -99,6 +99,19 @@ namespace Barotrauma.Networking
EntityEventInitial
}
[NetworkSerialize]
readonly record struct EntityPositionHeader(
bool IsItem,
UInt32 PrefabUintIdentifier,
UInt16 EntityId) : INetSerializableStruct
{
public static EntityPositionHeader FromEntity(Entity entity)
=> new (
IsItem: entity is Item,
PrefabUintIdentifier: entity is MapEntity me ? me.Prefab.UintIdentifier : 0,
EntityId: entity.ID);
}
enum TraitorMessageType
{
Server,
@@ -171,6 +171,8 @@ namespace Barotrauma.Networking
public bool ShouldCreateAnalyticsEvent
=> DisconnectReason is not (
DisconnectReason.Disconnected
or DisconnectReason.ServerShutdown
or DisconnectReason.ServerFull
or DisconnectReason.Banned
or DisconnectReason.Kicked
or DisconnectReason.TooManyFailedLogins
@@ -621,6 +621,15 @@ namespace Barotrauma
return stringValue.Split(';').Select(s => ParseTuple<T1, T2>(s, default)).ToArray();
}
public static Range<int> GetAttributeRange(this XElement element, string name, Range<int> defaultValue)
{
var attribute = element?.GetAttribute(name);
if (attribute is null) { return defaultValue; }
string stringValue = attribute.Value;
return string.IsNullOrEmpty(stringValue) ? defaultValue : ParseRange(stringValue);
}
public static string ElementInnerText(this XElement el)
{
StringBuilder str = new StringBuilder();
@@ -903,6 +912,37 @@ namespace Barotrauma
return floatArray;
}
// parse a range string, e.g "1-3" or "3"
public static Range<int> ParseRange(string rangeString)
{
if (string.IsNullOrWhiteSpace(rangeString)) { return GetDefault(rangeString); }
string[] split = rangeString.Split('-');
return split.Length switch
{
1 when TryParseInt(split[0], out int value) => new Range<int>(value, value),
2 when TryParseInt(split[0], out int min) && TryParseInt(split[1], out int max) && min < max => new Range<int>(min, max),
_ => GetDefault(rangeString)
};
static bool TryParseInt(string value, out int result)
{
if (!string.IsNullOrWhiteSpace(value))
{
return int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out result);
}
result = default;
return false;
}
static Range<int> GetDefault(string rangeString)
{
DebugConsole.ThrowError($"Error parsing range: \"{rangeString}\" (using default value 0-99)");
return new Range<int>(0, 99);
}
}
public static Identifier VariantOf(this XElement element) =>
element.GetAttributeIdentifier("inherit", element.GetAttributeIdentifier("variantof", ""));
@@ -64,7 +64,6 @@ namespace Barotrauma
EnableMouseLook = true,
ChatOpen = true,
CrewMenuOpen = true,
EditorDisclaimerShown = false,
ShowOffensiveServerPrompt = true,
TutorialSkipWarning = true,
CorpseDespawnDelay = 600,
@@ -132,7 +131,6 @@ namespace Barotrauma
public EnemyHealthBarMode ShowEnemyHealthBars;
public bool ChatOpen;
public bool CrewMenuOpen;
public bool EditorDisclaimerShown;
public bool ShowOffensiveServerPrompt;
public bool TutorialSkipWarning;
public int CorpseDespawnDelay;
@@ -75,6 +75,7 @@ namespace Barotrauma
case "targetgrandparent":
case "targetcontaineditem":
case "skillrequirement":
case "targetslot":
return false;
default:
return true;
@@ -1773,12 +1773,17 @@ namespace Barotrauma
void SpawnItem(ItemSpawnInfo chosenItemSpawnInfo)
{
Item parentItem = entity as Item;
if (user == null && parentItem != null)
{
// Set the user for projectiles spawned from status effects (e.g. flak shrapnels)
SetUser(parentItem.GetComponent<Projectile>()?.User);
}
switch (chosenItemSpawnInfo.SpawnPosition)
{
case ItemSpawnInfo.SpawnPositionType.This:
Entity.Spawner.AddItemToSpawnQueue(chosenItemSpawnInfo.ItemPrefab, position + Rand.Vector(chosenItemSpawnInfo.Spread, Rand.RandSync.Unsynced), onSpawned: newItem =>
{
Item parentItem = entity as Item;
Projectile projectile = newItem.GetComponent<Projectile>();
if (entity != null)
{
@@ -85,15 +85,6 @@ namespace Barotrauma.Steam
return Steamworks.SteamUGC.NumSubscribedItems;
}
public static PublishedFileId[] GetSubscribedItems()
{
if (!IsInitialized || !Steamworks.SteamClient.IsValid)
{
return Array.Empty<PublishedFileId>();
}
return Steamworks.SteamUGC.GetSubscribedItems();
}
public static bool UnlockAchievement(string achievementIdentifier) =>
UnlockAchievement(achievementIdentifier.ToIdentifier());
@@ -1,26 +1,20 @@
#nullable enable
using Barotrauma.IO;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Steamworks.Data;
using WorkshopItemSet = System.Collections.Generic.ISet<Steamworks.Ugc.Item>;
namespace Barotrauma.Steam
{
static partial class SteamManager
{
public const string WorkshopItemPreviewImageFolder = "Workshop";
public const string PreviewImageName = "PreviewImage.png";
public const string DefaultPreviewImagePath = "Content/DefaultWorkshopPreviewImage.png";
public static bool TryExtractSteamWorkshopId(this ContentPackage contentPackage, [NotNullWhen(true)]out SteamWorkshopId? workshopId)
{
workshopId = null;
@@ -47,16 +41,22 @@ namespace Barotrauma.Steam
private static async Task<WorkshopItemSet> GetWorkshopItems(Steamworks.Ugc.Query query, int? maxPages = null)
{
if (!IsInitialized) { return new HashSet<Steamworks.Ugc.Item>(); }
await Task.Yield();
query = query.WithKeyValueTags(true).WithLongDescription(true);
var set = new HashSet<Steamworks.Ugc.Item>(ItemEqualityComparer.Instance);
int prevSize = 0;
for (int i = 1; maxPages is null || i <= maxPages; i++)
for (int i = 1; i <= (maxPages ?? int.MaxValue); i++)
{
Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i);
if (page is null || !page.Value.Entries.Any()) { break; }
set.UnionWith(page.Value.Entries);
using Steamworks.Ugc.ResultPage? page = await query.GetPageAsync(i);
if (page is not { Entries: var entries }) { break; }
// This queries the results on the i-th page and stores them,
// using page.Entries directly would result in two GetQueryUGCResult calls
entries = entries.ToArray();
if (entries.None()) { break; }
set.UnionWith(entries);
if (set.Count == prevSize) { break; }
prevSize = set.Count;
@@ -66,10 +66,17 @@ namespace Barotrauma.Steam
// which can happen on items that are not visible to the currently
// logged in player (i.e. private & friends-only items)
set.RemoveWhere(it => it.ConsumerApp != AppID);
return set;
}
public static ImmutableHashSet<Steamworks.Data.PublishedFileId> GetSubscribedItemIds()
{
return IsInitialized
? Steamworks.SteamUGC.GetSubscribedItems().ToImmutableHashSet()
: ImmutableHashSet<Steamworks.Data.PublishedFileId>.Empty;
}
public static async Task<WorkshopItemSet> GetAllSubscribedItems()
{
if (!IsInitialized) { return new HashSet<Steamworks.Ugc.Item>(); }
@@ -98,14 +105,86 @@ namespace Barotrauma.Steam
.WhereUserPublished());
}
public static async Task<Steamworks.Ugc.Item?> GetItem(UInt64 itemId)
private static class SingleItemRequestPool
{
private static readonly object mutex = new();
private static readonly TimeSpan delayAfterNewRequest = TimeSpan.FromSeconds(0.5);
private static readonly HashSet<UInt64> ids = new();
private static Task<WorkshopItemSet>? currentBatch = null;
private static async Task<WorkshopItemSet> PrepareNewBatch()
{
// Wait for a bunch of requests to be made
await Task.Delay(delayAfterNewRequest);
Task<WorkshopItemSet> queryTask;
lock (mutex)
{
DebugConsole.Log(
$"{nameof(SteamManager)}.{nameof(Workshop)}.{nameof(SingleItemRequestPool)}: " +
$"Running batch of {ids.Count} requests");
queryTask = GetWorkshopItems(
Steamworks.Ugc.Query.All
.WithFileId(
ids
.Select(id => (Steamworks.Data.PublishedFileId)id)
.ToArray()));
ids.Clear();
// Immediately clear the current batch so the next request starts a new one
currentBatch = null;
}
return await queryTask;
}
public static async Task<Steamworks.Ugc.Item?> MakeRequest(UInt64 id)
{
Task<WorkshopItemSet> ourTask;
lock (mutex)
{
ids.Add(id);
if (currentBatch is not { IsCompleted: false })
{
// There is no currently pending batch, start a new one
currentBatch = Task.Run(PrepareNewBatch);
}
ourTask = currentBatch;
}
var items = await ourTask;
var result = items.FirstOrNull(it => it.Id == id);
return result;
}
}
/// <summary>
/// Fetches a Workshop item's metadata. This is batched to minimize Steamworks API calls.
/// The description of the returned item is truncated to save bandwidth.
/// </summary>
/// <param name="itemId">Workshop Item ID</param>
public static Task<Steamworks.Ugc.Item?> GetItem(UInt64 itemId)
=> SingleItemRequestPool.MakeRequest(itemId);
/// <summary>
/// Fetches a Workshop item's metadata in its own API call instead of batching.
/// This minimizes delay but needs to be used with caution to prevent rate limiting.
/// </summary>
/// <param name="itemId">Workshop Item ID</param>
/// <param name="withLongDescription">
/// If true, ask for the item's entire description, otherwise it'll be truncated.
/// </param>
public static async Task<Steamworks.Ugc.Item?> GetItemAsap(UInt64 itemId, bool withLongDescription = false)
{
if (!IsInitialized) { return null; }
var items = await GetWorkshopItems(
Steamworks.Ugc.Query.All
.WithFileId(itemId));
return items.Any() ? items.First() : (Steamworks.Ugc.Item?)null;
.WithFileId(itemId)
.WithLongDescription(withLongDescription));
return items.Any() ? items.First() : null;
}
public static async Task ForceRedownload(UInt64 itemId)
@@ -57,13 +57,12 @@ namespace Barotrauma
if (characterList.Any())
{
if (location?.Reputation is { } reputation && Faction.GetPlayerAffiliationStatus(reputation.Identifier, characterList) is FactionAffiliation.Positive)
if (location?.Faction is { } faction && Faction.GetPlayerAffiliationStatus(faction, characterList) is FactionAffiliation.Positive)
{
price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplierAffiliated));
}
price *= 1f - characterList.Max(static c => c.GetStatValue(StatTypes.ShipyardBuyMultiplier));
}
return (int)price;
}
}
@@ -270,10 +269,59 @@ namespace Barotrauma
}
}
internal partial class UpgradePrefab : UpgradeContentPrefab
internal readonly struct UpgradeResourceCost
{
public readonly int Amount;
private readonly ImmutableArray<Identifier> targetTags;
public readonly Range<int> TargetLevels;
public UpgradeResourceCost(ContentXElement element)
{
Amount = element.GetAttributeInt("amount", 0);
targetTags = element.GetAttributeIdentifierArray("item", Array.Empty<Identifier>())!.ToImmutableArray();
TargetLevels = element.GetAttributeRange("levels", new Range<int>(0, 99));
}
public bool AppliesForLevel(int currentLevel) => TargetLevels.Contains(currentLevel);
public bool AppliesForLevel(Range<int> newLevels) => newLevels.Start <= TargetLevels.End && newLevels.End >= TargetLevels.Start;
public bool MatchesItem(Item item) => MatchesItem(item.Prefab);
public bool MatchesItem(ItemPrefab item)
{
foreach (Identifier tag in targetTags)
{
if (tag.Equals(item.Identifier) || item.Tags.Contains(tag)) { return true; }
}
return false;
}
}
internal readonly struct ApplicableResourceCollection
{
public readonly ImmutableArray<ItemPrefab> MatchingItems;
public readonly UpgradeResourceCost Cost;
public readonly int Count;
public ApplicableResourceCollection(IEnumerable<ItemPrefab> matchingItems, int count, UpgradeResourceCost cost)
{
MatchingItems = matchingItems.ToImmutableArray();
Count = count;
Cost = cost;
}
public static ApplicableResourceCollection CreateFor(UpgradeResourceCost cost)
{
return new ApplicableResourceCollection(ItemPrefab.Prefabs.Where(cost.MatchesItem), cost.Amount, cost);
}
}
internal sealed partial class UpgradePrefab : UpgradeContentPrefab
{
public static readonly PrefabCollection<UpgradePrefab> Prefabs = new PrefabCollection<UpgradePrefab>(
onAdd: (prefab, isOverride) =>
onAdd: static (prefab, isOverride) =>
{
if (!prefab.SuppressWarnings && !isOverride)
{
@@ -342,6 +390,7 @@ namespace Barotrauma
private Dictionary<string, string[]> targetProperties { get; }
private readonly ImmutableArray<UpgradeMaxLevelMod> MaxLevelsMods;
public readonly ImmutableHashSet<UpgradeResourceCost> ResourceCosts;
public UpgradePrefab(ContentXElement element, UpgradeModulesFile file) : base(element, file)
{
@@ -354,6 +403,7 @@ namespace Barotrauma
var targetProperties = new Dictionary<string, string[]>();
var maxLevels = new List<UpgradeMaxLevelMod>();
var resourceCosts = new HashSet<UpgradeResourceCost>();
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", "");
if (!nameIdentifier.IsEmpty)
@@ -396,6 +446,11 @@ namespace Barotrauma
maxLevels.Add(new UpgradeMaxLevelMod(subElement));
break;
}
case "resourcecost":
{
resourceCosts.Add(new UpgradeResourceCost(subElement));
break;
}
#if CLIENT
case "decorativesprite":
{
@@ -427,6 +482,7 @@ namespace Barotrauma
this.targetProperties = targetProperties;
MaxLevelsMods = maxLevels.ToImmutableArray();
ResourceCosts = resourceCosts.ToImmutableHashSet();
upgradeCategoryIdentifiers = element.GetAttributeIdentifierArray("categories", Array.Empty<Identifier>())?
.ToImmutableHashSet() ?? ImmutableHashSet<Identifier>.Empty;
@@ -469,6 +525,58 @@ namespace Barotrauma
return GetMaxLevel(info) > 0;
}
public bool HasResourcesToUpgrade(Character? character, int currentLevel)
{
if (character is null) { return false; }
if (!ResourceCosts.Any()) { return true; }
List<Item> allItems = character.Inventory.FindAllItems(recursive: true);
return ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel)).All(cost => cost.Amount <= allItems.Count(cost.MatchesItem));
}
public bool TryTakeResources(Character character, int currentLevel)
{
IEnumerable<UpgradeResourceCost> costs = ResourceCosts.Where(cost => cost.AppliesForLevel(currentLevel));
if (!costs.Any()) { return true; }
List<Item> allItems = character.Inventory.FindAllItems(recursive: true);
HashSet<Item> itemsToRemove = new HashSet<Item>();
foreach (UpgradeResourceCost cost in costs)
{
int amountNeeded = cost.Amount;
foreach (Item item in allItems.Where(cost.MatchesItem))
{
itemsToRemove.Add(item);
amountNeeded--;
if (amountNeeded <= 0) { break; }
}
if (amountNeeded > 0) { return false; }
}
foreach (Item item in itemsToRemove)
{
item.Remove();
}
if (GameMain.IsMultiplayer) { character.Inventory.CreateNetworkEvent(); }
return true;
}
public ImmutableArray<ApplicableResourceCollection> GetApplicableResources(int level)
{
var applicableCosts = ResourceCosts.Where(cost => cost.AppliesForLevel(level)).ToImmutableHashSet();
var costs = applicableCosts.Any()
? applicableCosts.Select(ApplicableResourceCollection.CreateFor).ToImmutableArray()
: ImmutableArray<ApplicableResourceCollection>.Empty;
return costs;
}
public bool IsDisallowed(MapEntity item)
{
return item.DisallowedUpgradeSet.Contains(Identifier)
@@ -29,7 +29,7 @@ namespace Barotrauma
}
}
public bool Contains(in T v)
public readonly bool Contains(in T v)
=> start.CompareTo(v) <= 0 && end.CompareTo(v) >= 0;
private void VerifyStartLessThanEnd()
@@ -11,10 +11,10 @@ namespace Barotrauma
public abstract bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public static Success<T, TError> Success(T value)
public static Result<T, TError> Success(T value)
=> new Success<T, TError>(value);
public static Failure<T, TError> Failure(TError error)
public static Result<T, TError> Failure(TError error)
=> new Failure<T, TError>(error);
public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value);
+139 -34
View File
@@ -1,3 +1,40 @@
---------------------------------------------------------------------------------------------------------
v100.13.0.0
---------------------------------------------------------------------------------------------------------
- Fixes included in versions up to v0.21.0.0.
- Added translations for the endgame and faction content.
- Fixed reputation resetting between rounds.
- Added a round light component variant.
- Fixed z-fighting in HuskDistrict_Module_02.
- Fixed missing gaps in the Winterhalter wreck in the end level.
- Fixed end levels still sometimes including normal level geometry in saves started in the stable version.
---------------------------------------------------------------------------------------------------------
v100.12.0.0
---------------------------------------------------------------------------------------------------------
- Fixes included in versions up to v0.20.15.0.
- Fixed faction-specific bots not getting the correct ID card tags if they spawn for the first time in an outpost (= if you save and quit and reload in an outpost).
- The "x reputation needed to unlock some biome" text is hidden in the tab menu if some other faction has already unlocked the path.
- Fixed NPC following the player indefinitely in the "way to ascension" event in which you need to deliver husk eggs.
- Fixed "Campaigning" talent not giving a discount on upgrades.
- Fixed items appearing stolen if you enter another outpost that happens to have the same name as the one you stole the items from.
- The generic outpost NPC greeting lines ("nice to see a friendly face", etc) aren't used when you have negative reputation.
- Fixed ladders being drawn in front of the docking hatch in DockingModule_01..
- Fixed Subra assassination events working even if you've started the main husk cult chain (or even after you've hired Subra)
- Fixed submarine name being displayed as "Unknown" in the ending montage.
- Improvements/fixes to dialogs that are shown to multiple clients: disable the option buttons when another client chooses an option, and highlight the option that was chosen.
- Fixed outpost modules sometimes being placed in a way that makes them overlap with the sub.
- Adjusted the layout of the research beacon station to prevent the entrance from ending up in an impossible-to-reach spot, made beacon station docking ports visible on sonar.
- Fixed missing gaps in the end level's wrecks.
- Fixed ancient weapons not damaging doors anymore.
- Reputation rewards are displayed in the round summary.
- Fixed missions that are configured to give location reputation (= reputation for the faction controlling the location you got the mission from) sometimes not giving rep. Happened because the game assumed the correct location is the one at the start of the connection the mission takes place in, which may not be the case (the location may have been unlocked further on the map, or it may be at the end of the connection).
- Fixed mini nuke explosion sound.
- Fixed "could not determine reputation change" console warnings when checking reputation in the tab menu or round summary during the 1st round in MP.
- Fixed campaign map sometimes displaying incorrect reputation values in multiplayer.
---------------------------------------------------------------------------------------------------------
v100.11.0.0
---------------------------------------------------------------------------------------------------------
@@ -120,6 +157,74 @@ Test version of the faction overhaul:
- There's now always two paths from biome to another, one controlled by the Coalition and one by Separatists.
- Improvements to the campaign map.
---------------------------------------------------------------------------------------------------------
v0.21.0.0 (unstable)
---------------------------------------------------------------------------------------------------------
Changes:
- The "max missions" campaign setting is restricted to a maximum of 3.
- Revisited the skill requirements and the OnFailure conditions on welding tool, plasma cutter, flamer, and steam gun. Flamer and steam gun now apply burns on the user only when they don't have enough skills to use the item. All these items are now less efficient when used with low skills.
Bugfixes:
- Fixed high-quality items spawning earlier in the campaign when playing with a higher campaign difficulty setting.
- Fixed attacking with a melee weapon making you unable to turn (flip) for a while.
- Hull fixes to vanilla subs and wrecks.
- Fixed alien flares practically never spawning in ruins.
- Fixed status effects defined inside an attack definition still using the old OnUse/OnFailure logic instead of OnSuccess/OnFailure.
Modding:
- Addressed various inconsistencies, issues and limitations in how status effects are used in certain cases:
- Status effects defined for attacks with the type "UseTarget" now correctly target the use target, instead of the attacker like they used to.
- Changed the status effects of type "Character" to "UseTarget" for MeleeWeapon, Projectile, and Throwable components. The motivation behind this change is that previously we couldn't target the attacker at all within these item components, which might be desirable for some melee weapons.
- MODDERS, PLEASE NOTE: Effects that target "Character" in the previously mentioned components now affect the user - if that's not the intention, the target should be changed to "UseTarget".
---------------------------------------------------------------------------------------------------------
v0.20.16.1
---------------------------------------------------------------------------------------------------------
- Fixed console errors when firing a Flak Cannon using spreader ammo in multiplayer.
---------------------------------------------------------------------------------------------------------
v0.20.16.0
---------------------------------------------------------------------------------------------------------
- Fixed non-hitscan projectiles going through doors.
- Fixed electrical discharge coils hitting characters very unreliably, unless the character happens to be right next to a wall.
- Fixed makeshift shelves being containable in crates and cabinets, allowing for infinite recursive storage space.
- Fixed the effects of the "Grid Maintainer" and "Egghead" talents.
- Fixed incorrect "I Am That Guy" description (it gives a flat 20 skill bonus, not 20%).
- Fixed "Cruisin'" talent increasing fuel consumption by 10% instead of decreasing it by 20%.
- Optimized Steam Workshop queries done by the game (less bandwidth usage and stress on Steam's servers).
---------------------------------------------------------------------------------------------------------
v0.20.15.0
---------------------------------------------------------------------------------------------------------
- Fixed Revenge Squad talent doing nothing.
- Fixed Quickdraw talent's 8 second cooldown not working.
- Fixed flamer being fabricable without the appropriate talent.
- Fixed incorrect Aggressive Engineering talent description.
---------------------------------------------------------------------------------------------------------
v0.20.14.0
---------------------------------------------------------------------------------------------------------
- Fixed missing Rifle fabrication recipe.
- Fixed command devices (nav terminals, status monitors) not being in the electrical category, preventing "Better Than New" talent from having an effect on them.
- Fixed Graduation Ceremony still unlocking double the talents in multiplayer.
- Spawn husk eggs instead of husk syringes as creature loot.
- Cultist events require husk eggs instead of syringes.
- Adjusted shotgun fabrication recipe.
---------------------------------------------------------------------------------------------------------
v0.20.13.0
---------------------------------------------------------------------------------------------------------
- Fixed incorrect admin module sometimes being used in colonies, making it impossible to get to.
- Fixed a performance issue (character vitality logic being unnecessarily heavy) caused by some of the recent talent changes.
- Fixed inability to fabricate Rifle Bullets.
- Fixed contained items that have been configured to be hidden not being positioned correctly (meaning any positional effects done by the contained item would not work correctly).
---------------------------------------------------------------------------------------------------------
v0.20.12.0
---------------------------------------------------------------------------------------------------------
@@ -133,20 +238,19 @@ Talent overhaul:
Balance:
- Slightly adjusted values of handheld weapons.
- Power levels match cost better.
- Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls).
- Some tools are now slightly more damaging and viable as a last resort weapon (don't atually try to fight mudraptors with a wrench though).
- Improved ammo availability for basic weapons.
- Made some weapons available later in game, to increase feeling of progression.
- Damage values of weapons have been adjusted to be more in line with their cost
- Damage to structures has been revised (f.e. knives shouldn't be so efficient at cutting through walls).
- Improved ammo availability for basic weapons.
- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes.
- Made some weapons available later in the game to increase the feeling of progression.
- Slightly adjusted values of apparel (armor, clothing, diving suits) to better highlight strengths and weaknesses.
- Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances.
- PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths.
- Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair.
- All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit.
- Usage of a minimum difficulty level to have some weapons appear in stores only later in the game. Even some previously talent-only items can appear in stores now in very late biomes.
- Combat Diving Suit is now actually better for combat than the regular diving suit, due to higher damage resistances.
- PUCS no longer gives a bonus to speed when using Underwater Scooter, as it has plenty of other strengths.
- Mechanic's apparel now has higher laceration protection than Engineer's apparel, as that's typically the damage they'd get from failing to repair.
- All starter clothing gives less protection now, while some shop/npc clothing now gives some benefit.
- Chance of finding good/excellent/masterwork quality items in higher-difficulty levels.
- Plasma cutter is now much better at cutting.
- Rebalanced damage dealt by tools. Damage should be a bit higher overall.
- Rebalanced damage dealt by tools. Damage should be a bit higher overall. (don't actually try to fight mudraptors with a wrench though)
Tutorial improvements:
- A new campaign-integrated tutorial that teaches the basics of the campaign mode in the first outpost.
@@ -157,27 +261,27 @@ Tutorial improvements:
Changes and additions:
- New weapons: Rifle, Heavy Machine Gun, Machine Pistol, Harpoon Coil-Rifle.
- Flashlight can now be attached on all the ranged weapons held with two hands.
- Flashlight can now be attached to all ranged weapons held with two hands.
- Limit which submarines are available in each outpost: high-tier subs become available as you get further in the campaign, and the submarine class selection depends on the type of the outpost.
- Two new music tracks.
- Added a slider to the fabricator that can be used to select how many items to fabricate.
- Added an option to hide enemy health bars.
- Items' damage modifiers are shown in store tooltips.
- Added a button for treating all characters in one go to the medical clinic.
- Added a button for treating all characters in one go in the medical clinic.
- Breaches through the submarine's outer hull throws shrapnels that can cause minor damage to nearby characters, making monsters that can't get inside more of a threat to the crew (as opposed to just the submarine itself).
- Added a new honking scary random event to beacon stations.
- Added some particle, sound and light effects to water-sensitive materials and made them explode when they've been in water for 3 seconds, not immediately.
- Water-sensitive materials now explode when they've been in water for 3 seconds, not immediately. Added particle, sound and light effects.
- Affliction descriptions change depending on the strength of the affliction, and whether you're treating someone else or yourself.
- Added a button for opening the Steam Workshop to all tabs of the workshop menu.
- Added tooltips that explain how the bot spawn modes work to the server lobby.
- Added various new loot items to different creatures.
- Large monsters (Abyss monsters, Moloch, Watcher) drop items upon death.
- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version.
- Large monsters (Abyss monsters, Moloch, Watcher) now also drop items upon death.
- Husk eggs now come in two forms: Husk eggs with actual egg-like appearance and the syringe version. Typically syringes are crafted, so finding a syringe on creatures felt a bit out of place. The eggs look as yucky as youd expect.
- Made saline significantly less effective as a treatment for bloodloss to make blood packs more useful.
- Nerfed flak cannon's explosive ammo.
- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders: implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions.
- Emp damage now stuns and damages electrical characters (Fractalguardian and Defensebot). Modders note: its implemented as an affliction, so it's not tied to the "empstrength" attribute defined for explosions.
- Allow putting medium items (e.g. storage container) in medical and toxic cabinets.
- Some changes to wrecked item sprites (replacing the old low-res pictures with modified versions of the normal items' sprites).
- SMG can now be crafted.
- Optimized the server lobby: there was an issue in the logic that updates the microphone icon that caused the game to check available audio devices every frame.
- Optimized status monitors: previously some parts of their UI were always updated regardless if anyone is viewing the UI.
@@ -189,7 +293,7 @@ Multiplayer:
- Fixed inability to connect to IPv4 servers when IPv6 is disabled.
- Fixed occasional crashes when shutting down a server (for example with the error messages "pipe is broken" or "ChildServerRelay readTask did not run to completion").
- Fixed "no core packages in the list of mods the server has enabled" error when trying to join a server that's using a different version of the core package you have enabled.
- Fixed "Input contains duplicate packages" error still occuring if you try to join a server that has empty content packages when you don't have those packages yourself.
- Fixed "Input contains duplicate packages" error still occurring if you try to join a server that has empty content packages when you don't have those packages yourself.
- Fixed networking errors when the connection to the server is momentarily lost and then re-established.
- Added a cooldown to client name changes to prevent using it for spamming.
- Fixed bans issued with the "banaddress" command using a client's Steam ID not working.
@@ -214,12 +318,12 @@ Bugfixes:
- Fixed PUCS not beeping when you're underwater without a tank if you're inside a hull that has oxygen in it.
- Fixed some issues in sonar AITargets which made monsters hear the sonar when they shouldn't: switching to passive would immediately make the current directional ping cover 360 degrees, and whether the ping was directional or not would actually depend on whether the previous ping was directional, not what the mode is now.
- Fixed items getting autofilled into non-interactable containers in wrecks and outposts.
- Fixes to ID card tag issues in wrecks (prevented accessing the secure containers with the ID cards looted from the corpses).
- Fixed ID cards looted from the corpses of a wreck not giving access to the secure containers in the wreck.
- Fixed verifying file integrity on Steam resetting the server settings file.
- Fixed crashing if you try to open an access-restricted directory in the file selection dialog.
- Fixed a typo in physicorium shell's damage config, causing it to not do bleeding damage.
- Fixed money gain/lose popups no longer showing in the campaign.
- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. Caused e.g. drunkenness and bloodloss to never fully go away, causing issues with some talent effects.
- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. As a result e.g. drunkenness and bloodloss never fully went away, which caused issues with some talent effects.
- Fixed bots always opening the door/hatch they're trying to repair.
- Fixed power indicator not rotating with batteries.
- Fixed lights on welding tools and plasma cutters emitting light the next round if the round ends while using them.
@@ -227,8 +331,8 @@ Bugfixes:
- Fixed Berilia's bottom EDC not being wired to a supercapacitor and a loose wire between the flak cannon and the right supercapacitor.
- Fixed status effects targeting "NearbyCharacters" or "NearbyItems" being applied twice. Modders: if you used this, double the effects (e.g. damage) to get the same results as previously.
- Fixed a rounding error that caused Health Scanner HUD to display every level of bleeding below 100% as "minor".
- Fixed speech impediment from the husk infection making the bots unable to register any new targets autonomously (= without orders).
- Fixed bots having unintentionally long reaction times on reporting the issues, causing them to ignore any new enemies when they first envounter them.
- Fixed speech impediment from the husk infection making the bots unable to register any new targets on their own (= without being ordered).
- Fixed bots having unintentionally long reaction times on reporting the issues, causing them to ignore new enemies for a while when they first encounter them, unless being attacked.
- Fixed the default aim assist being 50% instead of 5%. Fixed aim assist not resetting when the reset button is pressed on the settings window.
- Fixed other players not seeing the spray particles when someone uses a sprayer in multiplayer.
- Fixed ability to "fire" (just dropping the projectile) hardpoints that are connected to a periscope and loader.
@@ -245,23 +349,23 @@ Bugfixes:
- Adjusted railgun, coilgun and double coilgun firing offsets to make the projectile spawn closer to the end of the barrel.
- Fixed loot sometimes spawning in vending machines' output slots.
- Fixed water level sometimes "flickering" up and down when water is leaking to a room from the left or right.
- Fixed resetting UI position doing nothing to equipped items' UIs (e.g. handheld status monitor).
- Fixed resetting the UI position doing nothing to equipped items' UIs (e.g. handheld status monitor).
- Fixed items equipped in the health interface slot being sellable.
- Fixed inconsistent view ranges of large turrets.
- Fixed SMG magazine shape being inconsistent with the shape of the mag well on the SMG sprite.
- Fixed character portrait and health bar buttons being clickable (despite being hidden) when the health interface is open.
- Attempt to fix occasional crashes due to location store being null when teleporting from location to another with console commands.
- Fixed occasional crashes due to location store being null when teleporting from location to another with console commands.
- Fixes to impact-sensitive items exploding at the start of the round (e.g. at the start of explosive transport missions or when purchasing explosives).
- Attempt to fix bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret.
- Fixed bots occasionally being unable to operate turrets when starting a new round until they're re-ordered to man the turret.
- Fixed focus staying on the highlighted item/character indefinitely if you keep holding LMB, even if you're outside interaction range.
- Prevented spawning of genetic materials outside creature inventories when the inventory size was too small, by increasing inventory sizes.
- Fixed some creatures not having enough space in their inventory for the genetic materials to spawn into.
- Fixed minerals still sometimes being placed outside the level in mineral missions.
- Yet another fix to cave tunnels sometimes being too narrow to pass through.
- Fixed cave tunnels sometimes being too narrow to pass through.
- Fixed "man and his raptor" outpost event giving 1000 marks in an incorrect branch of the dialog (the one where you immediately accept the NPC on board, instead of the one where the NPC says they'll pay you 1000 mk).
- Fixed cases of interaction texts for focused item (most notoriously, the planter) not being updated correctly.
- Fixed "snap to grid" causing door gaps to get misaligned.
- Fixed weird equipping behavior on fruit and paints, causing them to be equipped in both hands when trying to unequip.
- Fixed junction boxes not getting damaged by water since the power rework.
- Fixed junction boxes not getting damaged by water.
- Fixed opiate withdrawal only reducing down to 20%, but never fully healing by itself.
- Fixed engines reverting back to the non-damaged sprite when they're damaged badly enough that the sprite starts shaking.
- Fixed walls being set up incorrectly in vertical abandoned outpost hallway modules, causing them to stick out into the connected modules.
@@ -272,16 +376,16 @@ Bugfixes:
Modding:
- Added a button to the main menu that can be used to update all installed mods when there's updates available.
- Mods with errors can no longer be enabled.
- Mods with errors can no longer be enabled. This change should encourage modders to fix errors in their mods and report bugs, as well as discourage players from ignoring errors.
- Removed most of the debug console error spam seen when launching the game or opening the settings menu when faulty mods are installed.
- Fixed mods failing to show up in the mods list at all when they have certain kinds of errors.
- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral: always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the requirements are matched.
- Implemented the status effect type "OnSuccess" where "OnUse" was used instead. Changed "OnUse" to be neutral, meaning it always triggers, regardless of the (skill) requirements. You may need to switch using "OnSuccess" instead of "OnUse", if it's intended for the status effect to trigger only when the (skill) requirements are matched.
- Fixed increasing an item's HealthMultiplier making the items appear damaged in existing subs/saves (e.g. if you doubled an item's maximum condition, the items would remain in the old maximum condition and appear 50% damaged).
- Fixed crashing if a talent is triggered when the character receives some affliction, and that talent applies the same affliction on the character.
- Fixed crashing if the ingredient of a fabrication recipe can't be found.
- Fixed inability to sync properties of ItemComponents that the item has multiple of (meaning that it was only possible to e.g. edit the light color of the item's first LightComponent if it has multiple).
- Allow 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses).
- Allow 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration).
- Added 'launchimpulse' on RangedWeapon to affect projectile's speed (sum of launch impulses).
- Added 'penetration' on RangedWeapon to affect projectile's penetration (sum of penetration).
- Added 'DontApplyToHands' property to Propulsion, preventing extra force applying to hands when the item is held in hands (instead applying only to the character's whole body).
- Added a skill requirement conditional for StatusEffect, example: <Conditional skillrequirement="true" weapons="lt 35" /> to make a status effect occur only if the target has less than 35 weapon skill.
- Added ReloadSkillRequirement and ReloadNoSkill to RangedWeapon. E.g. a weapon with reload=2s, ReloadSkillRequirement=40, ReloadNoSkill=5s will have a character with 20 weapons skill reload at 3.5 s.
@@ -290,8 +394,9 @@ Modding:
- Added UseEnvironment.None to Propulsion component.
- Fixed the debug console command "head" causing the character to disappear. The command can be used for changing the appearance of the character at runtime.
- Status effects of type "OnUse" on projectiles now trigger when the projectile is launched. Previously it launched when the projectile hit the target. Use OnImpact (or OnSuccess/OnFailure) when you want something to happen when the projectile hits the target.
- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition off, because it'd override the affliction specific value.
- Added an option to multiply the damage by max vitality (relative damage) per affliction definition, in addition to the "multiplyAfflictionsByMaxVitality" attribute defined for the status effects. If you want to define it for an affliction separately, leave the status effect level definition undefined, because it'd override the affliction specific value.
- Fixed item's OnSpawn effects being applied twice.
- Fixed item components that inherit the status effects from another item component (e.g. the medical syringes) triggering the status effects twice when the effect is triggered via the item and not via the item component. Didnt cause issues with the vanilla items, but might affect some mods. If your item e.g. suddenly does only half of the damage after the update, its possible that it was affected by the bug. Just double the effect to fix it.
---------------------------------------------------------------------------------------------------------
v0.19.14.0