Build 0.20.7.0

This commit is contained in:
Markus Isberg
2022-11-18 18:13:38 +02:00
parent 8c8fd865c5
commit ecb6d40b4b
111 changed files with 1346 additions and 701 deletions
@@ -102,6 +102,8 @@ namespace Barotrauma
private bool inDetectable;
public double InDetectableSetTime;
/// <summary>
/// Should be reset to false each frame and kept indetectable by e.g. a status effect.
/// </summary>
@@ -115,7 +117,8 @@ namespace Barotrauma
{
inDetectable = value;
if (inDetectable)
{
{
InDetectableSetTime = Timing.TotalTime;
NeedsUpdate = true;
}
}
@@ -257,9 +260,14 @@ namespace Barotrauma
SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime);
}
public bool HasSector()
{
return sectorRad < MathHelper.TwoPi;
}
public bool IsWithinSector(Vector2 worldPosition)
{
if (sectorRad >= MathHelper.TwoPi) { return true; }
if (!HasSector()) { return true; }
Vector2 diff = worldPosition - WorldPosition;
return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f;
}
@@ -1083,7 +1083,7 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
else
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
SelectedAiTarget = owner.AiTarget;
}
@@ -2147,7 +2147,6 @@ namespace Barotrauma
if (SelectedAiTarget?.Entity == null) { return false; }
if (AttackLimb?.attack == null) { return false; }
if (damageTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (wallTarget != null)
{
// If the selected target is not the wall target, make the wall target the selected target.
@@ -2156,9 +2155,10 @@ namespace Barotrauma
{
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority);
State = AIState.Attack;
return true;
}
}
if (damageTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
@@ -3810,6 +3810,7 @@ namespace Barotrauma
{
SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority);
State = AIState.Attack;
return false;
}
}
}
@@ -3881,9 +3882,8 @@ namespace Barotrauma
return targetLimb;
}
private Character GetOwner(Item item)
private static Character GetOwner(Item item)
{
// If the item is held by a character, attack the character instead.
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
{
@@ -413,28 +413,10 @@ namespace Barotrauma
}
}
}
private void ApplyTreatment(Affliction affliction, Item item)
{
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
bool remove = false;
foreach (ItemComponent ic in item.Components)
{
if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; }
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: character);
if (ic.DeleteOnUse)
{
remove = true;
}
}
if (remove)
{
Entity.Spawner?.AddItemToRemoveQueue(item);
}
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
}
protected override bool CheckObjectiveSpecific()
@@ -1371,7 +1371,7 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition;
targetMovement = new Vector2(diff.X, 0.0f);
TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left;
@@ -1386,15 +1386,25 @@ namespace Barotrauma
float prevVitality = target.Vitality;
bool wasCritical = prevVitality < 0.0f;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
{
target.Oxygen += deltaTime * 0.5f; //Stabilize them
}
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel("medical");
if (GameMain.NetworkMember is not { IsClient: true })
{
if (cprBoost >= 1f)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
}
}
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
if (cprAnimTimer > 15.0f && targetHead != null && head != null)
{
@@ -1405,23 +1415,15 @@ namespace Barotrauma
torso.PullJointEnabled = true;
//Serverside code
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
if (GameMain.NetworkMember is not { IsClient: true })
{
if (target.Oxygen < -10.0f)
{
if (cprBoost >= 1f)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
}
else
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
}
}
@@ -25,6 +25,8 @@ namespace Barotrauma
FriendlyNPC = 3
}
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{
public readonly static List<Character> CharacterList = new List<Character>();
@@ -347,7 +349,7 @@ namespace Barotrauma
private readonly Dictionary<ItemPrefab, double> itemSelectedDurations = new Dictionary<ItemPrefab, double>();
private double itemSelectedTime;
public float InvisibleTimer;
public float InvisibleTimer { get; set; }
public readonly CharacterPrefab Prefab;
@@ -1372,7 +1374,28 @@ namespace Barotrauma
tags.RemoveWhere(t => t.StartsWith("variant"));
tags.Add($"variant{headId.Value}".ToIdentifier());
}
var oldHeadInfo = Info.Head;
Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
if (hairIndex == -1)
{
Info.Head.HairIndex = oldHeadInfo.HairIndex;
}
if (beardIndex == -1)
{
Info.Head.BeardIndex = oldHeadInfo.BeardIndex;
}
if (moustacheIndex == -1)
{
Info.Head.MoustacheIndex = oldHeadInfo.MoustacheIndex;
}
if (faceAttachmentIndex == -1)
{
Info.Head.FaceAttachmentIndex = oldHeadInfo.FaceAttachmentIndex;
}
Info.Head.SkinColor = oldHeadInfo.SkinColor;
Info.Head.HairColor = oldHeadInfo.HairColor;
Info.Head.FacialHairColor = oldHeadInfo.FacialHairColor;
Info.CheckColors();
#if CLIENT
head.RecreateSprites();
#endif
@@ -3050,7 +3073,8 @@ namespace Barotrauma
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (aiTarget != null)
//wait 0.1 seconds so status effects that continuously set InDetectable to true can keep the character InDetectable
if (aiTarget != null && Timing.TotalTime > aiTarget.InDetectableSetTime + 0.1f)
{
aiTarget.InDetectable = false;
}
@@ -5087,25 +5111,48 @@ namespace Barotrauma
return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
}
private readonly Dictionary<Identifier, float> abilityResistances = new Dictionary<Identifier, float>();
private readonly Dictionary<TalentResistanceIdentifier, float> abilityResistances = new();
public float GetAbilityResistance(AfflictionPrefab affliction)
{
return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f;
float resistance = 0f;
bool hadResistance = false;
foreach (var (key, value) in abilityResistances)
{
if (key.ResistanceIdentifier == affliction.AfflictionType ||
key.ResistanceIdentifier == affliction.Identifier)
{
resistance += value;
hadResistance = true;
}
}
return hadResistance ? resistance : 1f;
}
public void ChangeAbilityResistance(Identifier resistanceId, float value)
public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value)
{
if (abilityResistances.ContainsKey(resistanceId))
if (!MathUtils.IsValid(value))
{
abilityResistances[resistanceId] *= value;
#if DEBUG
DebugConsole.ThrowError($"Attempted to set ability resistance to an invalid value ({value})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
if (abilityResistances.ContainsKey(identifier))
{
abilityResistances[identifier] *= value;
}
else
{
abilityResistances.Add(resistanceId, value);
abilityResistances.Add(identifier, value);
}
}
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>
@@ -63,27 +63,34 @@ namespace Barotrauma
public readonly CharacterInfo CharacterInfo;
public readonly HeadPreset Preset;
private int hairIndex;
public int HairIndex { get; set; }
public int HairIndex
private int? hairWithHatIndex;
public void SetHairWithHatIndex()
{
get => hairIndex;
set
if (CharacterInfo.Hairs is null)
{
hairIndex = value;
if (CharacterInfo.Hairs is null)
if (HairIndex == -1)
{
HairWithHatIndex = value;
return;
#if DEBUG
DebugConsole.ThrowError("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#else
DebugConsole.AddWarning("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#endif
}
HairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", hairIndex) ?? -1;
if (HairWithHatIndex < 0 || HairWithHatIndex >= CharacterInfo.Hairs.Count)
hairWithHatIndex = HairIndex;
}
else
{
hairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", HairIndex) ?? -1;
if (hairWithHatIndex < 0 || hairWithHatIndex >= CharacterInfo.Hairs.Count)
{
HairWithHatIndex = hairIndex;
hairWithHatIndex = HairIndex;
}
}
}
public int HairWithHatIndex { get; private set; }
public int BeardIndex;
public int MoustacheIndex;
public int FaceAttachmentIndex;
@@ -99,26 +106,29 @@ namespace Barotrauma
get
{
if (CharacterInfo.Hairs == null) { return null; }
if (hairIndex >= CharacterInfo.Hairs.Count)
if (HairIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairIndex})");
DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(hairIndex);
return CharacterInfo.Hairs.ElementAtOrDefault(HairIndex);
}
}
public ContentXElement HairWithHatElement
{
get
{
if (CharacterInfo.Hairs == null) { return null; }
if (HairWithHatIndex >= CharacterInfo.Hairs.Count)
if (hairWithHatIndex == null)
{
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairWithHatIndex})");
SetHairWithHatIndex();
}
return CharacterInfo.Hairs.ElementAtOrDefault(HairWithHatIndex);
if (CharacterInfo.Hairs == null) { return null; }
if (hairWithHatIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairWithHatIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(hairWithHatIndex.Value);
}
}
public ContentXElement BeardElement
{
get
@@ -711,7 +721,7 @@ namespace Barotrauma
private bool IsColorValid(in Color clr)
=> clr.R != 0 || clr.G != 0 || clr.B != 0;
private void CheckColors()
public void CheckColors()
{
if (!IsColorValid(Head.HairColor))
{
@@ -27,6 +27,14 @@ namespace Barotrauma
get { return _strength; }
set
{
if (!MathUtils.IsValid(value))
{
#if DEBUG
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({value})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
if (_nonClampedStrength < 0 && value > 0)
{
_nonClampedStrength = value;
@@ -440,9 +448,9 @@ namespace Barotrauma
{
statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
if (characterHealth?.Character?.AnimController?.Limbs != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
statusEffect.Apply(type, deltaTime, targetLimb.character, targets: targetLimb.character.AnimController.Limbs);
statusEffect.Apply(type, deltaTime, characterHealth.Character, targets: characterHealth.Character.AnimController.Limbs);
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
@@ -405,45 +405,53 @@ namespace Barotrauma
var root = doc.Root.FromPackage(pathToAppendage.ContentPackage);
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
//the IDs may need to be offset if the character has other extra appendages (e.g. from gene splicing)
//that take up the IDs of this appendage
int idOffset = 0;
foreach (var jointElement in root.GetChildElements("joint"))
{
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement))
if (!limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { continue; }
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams)
{
// Ensure that we have a valid id for the new limb
ID = ragdoll.Limbs.Length
};
jointParams.Limb2 = appendageLimbParams.ID;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
//the joint attaches to a limb outside the character's normal limb count = to another part of the appendage
// -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
if (jointParams.Limb1 >= ragdoll.RagdollParams.Limbs.Count)
{
jointParams.Limb1 += idOffset;
}
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams);
if (idOffset == 0)
{
idOffset = ragdoll.Limbs.Length - appendageLimbParams.ID;
}
jointParams.Limb2 = appendageLimbParams.ID = ragdoll.Limbs.Length;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
}
return appendage;
}
@@ -887,7 +887,7 @@ namespace Barotrauma
//clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
//the character dies of oxygen deprivation in 100 seconds after losing consciousness
OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
}
else
{
@@ -895,15 +895,21 @@ namespace Barotrauma
float increaseSpeed = 10.0f;
decreaseSpeed *= (1f - oxygenlowResistance);
increaseSpeed *= (1f + oxygenlowResistance);
float holdBreathMultiplier = 1f + GetStatValue(StatTypes.HoldBreathMultiplier);
decreaseSpeed *= holdBreathMultiplier;
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier);
if (holdBreathMultiplier <= -1.0f)
{
OxygenAmount = -100.0f;
}
else
{
decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier);
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
}
}
UpdateOxygenProjSpecific(prevOxygen, deltaTime);
}
partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,5 +1,5 @@
using System.Collections.Immutable;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
namespace Barotrauma.Abilities
{
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGainSimultaneousSkill : CharacterAbility
{
@@ -15,6 +12,10 @@ namespace Barotrauma.Abilities
skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", "");
ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true);
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
if (skillIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -23,13 +24,12 @@ namespace Barotrauma.Abilities
{
if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; }
Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier;
if (targetAllies)
{
foreach (Character character in Character.GetFriendlyCrew(Character))
foreach (Character otherCharacter in Character.GetFriendlyCrew(Character))
{
if (character == Character) { continue; }
Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
if (otherCharacter == Character) { continue; }
otherCharacter.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
}
}
else
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveAffliction : CharacterAbility
{
@@ -18,7 +16,7 @@ namespace Barotrauma.Abilities
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.");
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveFlag : CharacterAbility
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveMoney : CharacterAbility
{
@@ -13,6 +11,11 @@ namespace Barotrauma.Abilities
{
amount = abilityElement.GetAttributeInt("amount", 0);
scalingStatIdentifier = abilityElement.GetAttributeIdentifier("scalingstatidentifier", Identifier.Empty);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.");
}
}
private void ApplyEffectSpecific(Character targetCharacter)
@@ -1,6 +1,4 @@
using System;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
public enum PermanentStatPlaceholder
{
@@ -28,6 +26,10 @@ namespace Barotrauma.Abilities
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.");
}
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f);
@@ -11,6 +11,14 @@ namespace Barotrauma.Abilities
{
factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
amount = abilityElement.GetAttributeFloat("amount", 0f);
if (factionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.");
}
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.");
}
}
protected override void ApplyEffect()
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveResistance : CharacterAbility
{
@@ -10,17 +8,23 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty));
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f);
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.");
}
if (MathUtils.NearlyEqual(multiplier, 1))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.");
}
}
public override void InitializeAbility(bool addingFirstTime)
{
Character.ChangeAbilityResistance(resistanceId, multiplier);
TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier);
Character.ChangeAbilityResistance(identifier, multiplier);
}
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveStat : CharacterAbility
{
@@ -1,7 +1,4 @@
using Barotrauma.Extensions;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGiveTalentPoints : CharacterAbility
{
@@ -9,7 +6,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
}
}
public override void InitializeAbility(bool addingFirstTime)
@@ -9,6 +9,10 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
}
}
public override void InitializeAbility(bool addingFirstTime)
@@ -1,6 +1,4 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -6,6 +6,10 @@ namespace Barotrauma.Abilities
public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -1,7 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyFlag : CharacterAbility
{
@@ -1,23 +1,25 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyResistance : CharacterAbility
{
private readonly Identifier resistanceId;
private readonly float resistance;
private readonly float multiplier;
bool lastState;
public override bool AllowClientSimulation => true;
// should probably be split to different classes
public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", "");
resistance = abilityElement.GetAttributeFloat("resistance", 1f);
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty));
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f);
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.");
}
if (MathUtils.NearlyEqual(multiplier, 1.0f))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.");
}
}
@@ -25,7 +27,15 @@ namespace Barotrauma.Abilities
{
if (conditionsMatched != lastState)
{
Character.ChangeAbilityResistance(resistanceId, conditionsMatched ? resistance : 1 / resistance);
TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier);
if (conditionsMatched)
{
Character.ChangeAbilityResistance(identifier, multiplier);
}
else
{
Character.RemoveAbilityResistance(identifier);
}
lastState = conditionsMatched;
}
}
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyStat : CharacterAbility
{
@@ -1,5 +1,4 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,5 +1,4 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityModifyValue : CharacterAbility
{
@@ -11,6 +9,10 @@ namespace Barotrauma.Abilities
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f))
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityPutItem : CharacterAbility
{
@@ -9,7 +9,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.");
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityRevive : CharacterAbility
{
@@ -11,6 +11,10 @@ namespace Barotrauma.Abilities
{
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
value = abilityElement.GetAttributeInt("value", 0);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.");
}
}
public override void InitializeAbility(bool addingFirstTime)
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityApprenticeship : CharacterAbility
{
@@ -1,20 +1,17 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityAtmosMachine : CharacterAbility
{
private readonly float addedValue;
private readonly float multiplyValue;
private readonly Identifier[] tags;
private readonly int maxMultiplyCount;
public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
tags = abilityElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>());
maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue);
}
@@ -1,11 +1,8 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityBountyHunter : CharacterAbility
{
private float vitalityPercentage;
private readonly float vitalityPercentage;
public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityMultitasker : CharacterAbility
{
@@ -1,12 +1,10 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityPsychoClown : CharacterAbility
{
private StatTypes statType;
private float maxValue;
private string afflictionIdentifier;
private readonly StatTypes statType;
private readonly float maxValue;
private readonly string afflictionIdentifier;
private float lastValue = 0f;
public override bool AllowClientSimulation => true;
@@ -1,8 +1,5 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,8 +1,8 @@
#nullable enable
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Extensions;
namespace Barotrauma.Abilities
{
@@ -66,8 +66,8 @@ namespace Barotrauma
IEnumerable<TalentSubTree> blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)),
requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier));
return requiredSubTrees.All(tst => tst.IsCompleted(selectedTalents)) && // check if we meet requirements
!blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents)); // check if any other talent trees are blocking this one
return requiredSubTrees.All(tst => tst.HasEnoughTalents(selectedTalents)) && // check if we meet requirements
!blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents) && !tst.HasMaxTalents(selectedTalents)); // check if any other talent trees are blocking this one
}
// i hate this function - markus
@@ -128,16 +128,26 @@ namespace Barotrauma
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
{
if (character?.Info?.Job.Prefab == null) { return false; }
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
foreach (var subTree in talentTree!.TalentSubTrees)
{
if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; }
foreach (var talentOptionStage in subTree.TalentOptionStages)
{
bool hasTalentInThisTier = talentOptionStage.HasEnoughTalents(selectedTalents);
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
{
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
if (!optionStageCompleted)
{
break;
}
/*bool hasTalentInThisTier = talentOptionStage.HasMaxTalents(selectedTalents);
if (!hasTalentInThisTier)
{
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
@@ -145,7 +155,7 @@ namespace Barotrauma
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
break;
}
}*/
}
}
@@ -196,7 +206,8 @@ namespace Barotrauma
public readonly ImmutableHashSet<Identifier> RequiredTrees;
public readonly ImmutableHashSet<Identifier> BlockedTrees;
public bool IsCompleted(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents));
public bool HasEnoughTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents));
public bool HasMaxTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasMaxTalents(talents));
public bool HasAnyTalent(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents));
public TalentSubTree(ContentXElement subTreeElement)
@@ -228,6 +239,13 @@ namespace Barotrauma
public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
/// <summary>
/// How many talents need to be unlocked to consider this tree completed
/// </summary>
public readonly int RequiredTalents;
/// <summary>
/// How many talents can be unlocked in total
/// </summary>
public readonly int MaxChosenTalents;
/// <summary>
@@ -236,8 +254,9 @@ namespace Barotrauma
/// </summary>
public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents = new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents;
public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= RequiredTalents;
public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= RequiredTalents;
public bool HasMaxTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
// No LINQ
public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
@@ -267,10 +286,15 @@ namespace Barotrauma
public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
{
MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1);
MaxChosenTalents = talentOptionsElement.GetAttributeInt(nameof(MaxChosenTalents), 1);
RequiredTalents = talentOptionsElement.GetAttributeInt(nameof(RequiredTalents), MaxChosenTalents);
if (RequiredTalents > MaxChosenTalents)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.");
}
HashSet<Identifier> identifiers = new HashSet<Identifier>();
foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements())
{
Identifier elementName = talentOptionElement.Name.ToIdentifier();
@@ -293,6 +317,15 @@ namespace Barotrauma
}
talentIdentifiers = identifiers.ToImmutableHashSet();
if (RequiredTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.");
}
if (MaxChosenTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.");
}
}
}
}
@@ -22,7 +22,7 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier OrderTargetTag { get; set; }
[Serialize(OrderPriority.Top, IsPropertySaveable.Yes)]
[Serialize(OrderPriority.Any, IsPropertySaveable.Yes)]
public OrderPriority Priority { get; set; }
public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -43,6 +43,9 @@ partial class UIHighlightAction : EventAction
[Serialize(true, IsPropertySaveable.Yes)]
public bool Bounce { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool HighlightMultiple { get; set; }
private bool isFinished;
public UIHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -207,25 +207,23 @@ namespace Barotrauma
level.StartLocation.Connections.ForEach(c => c.Locked = false);
}
}
AddChildEvents(initialEventSet);
void AddChildEvents(EventSet eventSet)
}
RegisterNonRepeatableChildEvents(initialEventSet);
void RegisterNonRepeatableChildEvents(EventSet eventSet)
{
if (eventSet == null) { return; }
if (eventSet.OncePerLevel)
{
if (eventSet == null) { return; }
if (eventSet.OncePerOutpost)
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
{
nonRepeatableEvents.Add(ep);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
{
AddChildEvents(childSet);
nonRepeatableEvents.Add(ep);
}
}
}
foreach (EventSet childSet in eventSet.ChildSets)
{
RegisterNonRepeatableChildEvents(childSet);
}
}
}
PreloadContent(GetFilesToPreload());
@@ -374,26 +372,18 @@ namespace Barotrauma
/// </summary>
public void RegisterEventHistory()
{
if (level?.LevelData != null)
if (level?.LevelData == null) { return; }
level.LevelData.EventsExhausted = true;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{
level.LevelData.EventsExhausted = true;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
}
foreach (var usedUniqueSet in usedUniqueSets)
{
if (!level.LevelData.UsedUniqueSets.Contains(usedUniqueSet.Identifier))
{
level.LevelData.UsedUniqueSets.Add(usedUniqueSet.Identifier);
}
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
}
public void SkipEventCooldown()
@@ -418,16 +408,11 @@ namespace Barotrauma
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
if (eventSet.Unique && !usedUniqueSets.Contains(eventSet))
{
usedUniqueSets.Add(eventSet);
}
int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin)
{
applyCount = level.Ruins.Count();
applyCount = level.Ruins.Count;
foreach (var ruin in level.Ruins)
{
spawnPosFilter.Add(pos => pos.Ruin == ruin);
@@ -435,7 +420,7 @@ namespace Barotrauma
}
else if (eventSet.PerCave)
{
applyCount = level.Caves.Count();
applyCount = level.Caves.Count;
foreach (var cave in level.Caves)
{
spawnPosFilter.Add(pos => pos.Cave == cave);
@@ -452,8 +437,8 @@ namespace Barotrauma
}
bool isPrefabSuitable(EventPrefab e)
=> e.BiomeIdentifier.IsEmpty ||
e.BiomeIdentifier == level.LevelData?.Biome?.Identifier;
=> (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
!level.LevelData.NonRepeatableEvents.Contains(e);
foreach (var subEventPrefab in eventSet.EventPrefabs)
{
@@ -610,8 +595,7 @@ namespace Barotrauma
return
level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty &&
level.LevelData.Type == eventSet.LevelType &&
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier) &&
(!eventSet.Unique || !level.LevelData.UsedUniqueSets.Contains(eventSet.Identifier));
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
}
private bool IsValidForLocation(EventSet eventSet, Location location)
@@ -114,15 +114,9 @@ namespace Barotrauma
public readonly bool DisableInHuntingGrounds;
/// <summary>
/// If true, events from this set shouldn't be selected again as long as they remain in <see cref="LevelData.NonRepeatableEvents"/> which has a limited size.
/// Use <see cref="Unique"/> to prevent selecting the whole set again altogether.
/// If true, events from this set can only occur once in the level.
/// </summary>
public readonly bool OncePerOutpost;
/// <summary>
/// If true, the whole set can only be selected once for a level.
/// </summary>
public readonly bool Unique;
public readonly bool OncePerLevel;
public readonly bool DelayWhenCrewAway;
@@ -289,8 +283,7 @@ namespace Barotrauma
DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck));
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
Unique = element.GetAttributeBool("unique", false);
OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false));
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
ResetTime = element.GetAttributeFloat("resettime", 0);
@@ -386,23 +386,27 @@ namespace Barotrauma
#if CLIENT
foreach (Character character in crewCharacters)
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
character.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
character.Info?.GiveExperience(experienceGain, isMissionExperience: true);
GiveMissionExperience(character.Info);
}
#else
foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients)
{
//give the experience to the stored characterinfo if the client isn't currently controlling a character
CharacterInfo info = c.Character?.Info ?? c.CharacterInfo;
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo);
}
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
{
GiveMissionExperience(bot.Info);
}
#endif
void GiveMissionExperience(CharacterInfo info)
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
}
// apply money gains afterwards to prevent them from affecting XP gains
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
@@ -306,14 +306,6 @@ namespace Barotrauma
return validContainers;
}
private static readonly (int quality, float commonness)[] qualityCommonnesses = new (int quality, float commonness)[Quality.MaxQuality + 1]
{
(0, 1.0f),
(1, 0.0f),
(2, 0.0f),
(3, 0.0f),
};
private static List<Item> CreateItems(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{
List<Item> newItems = new List<Item>();
@@ -336,11 +328,7 @@ namespace Barotrauma
break;
}
var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab);
int quality =
existingItem?.Quality ??
ToolBox.SelectWeightedRandom(
qualityCommonnesses.Select(q => q.quality).ToList(),
qualityCommonnesses.Select(q => q.commonness).ToList(), Rand.RandSync.ServerAndClient);
int quality = existingItem?.Quality ?? Quality.GetSpawnedItemQuality(validContainer.Key.Item.Submarine, Level.Loaded, Rand.RandSync.ServerAndClient);
if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false)
{
@@ -276,7 +276,7 @@ namespace Barotrauma.Items.Components
}
}
//get all walls within range
//get all walls within range the arc could potentially hit
List<Entity> entitiesInRange = new List<Entity>(100);
foreach (Structure structure in Structure.WallList)
{
@@ -284,10 +284,10 @@ namespace Barotrauma.Items.Components
if (structure.Submarine != null&& !submarinesInRange.Contains(structure.Submarine)) { continue; }
var structureWorldRect = structure.WorldRect;
if (worldPosition.X < structureWorldRect.X - range) continue;
if (worldPosition.X > structureWorldRect.Right + range) continue;
if (worldPosition.Y > structureWorldRect.Y + range) continue;
if (worldPosition.Y < structureWorldRect.Y -structureWorldRect.Height - range) continue;
if (worldPosition.X < structureWorldRect.X - range) { continue; }
if (worldPosition.X > structureWorldRect.Right + range) { continue; }
if (worldPosition.Y > structureWorldRect.Y + range) { continue; }
if (worldPosition.Y < structureWorldRect.Y - structureWorldRect.Height - range) { continue; }
if (structure.Submarine != null)
{
@@ -317,6 +317,7 @@ namespace Barotrauma.Items.Components
nodes.Add(new Node(worldPosition, -1));
}
//get all characters within range the arc could potentially hit
float totalRange = RaycastRange + range;
foreach (Character character in Character.CharacterList)
{
@@ -325,11 +326,20 @@ namespace Barotrauma.Items.Components
if (OutdoorsOnly && character.Submarine != null) { continue; }
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; }
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls ||
(RaycastRange > 0.0f && MathUtils.LineToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition) < range * range * RangeMultiplierInWalls))
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls)
{
entitiesInRange.Add(character);
charactersInRange.Add((character, nodes[0]));
}
//if the weapon does a raycast, check distance to the ray too (not just the end of the ray)
if (RaycastRange > 0)
{
float distSqr = MathUtils.LineSegmentToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition);
//if the distance from the initial raycast to the character is small (e.g. goes through the character), we know it must hit
if (distSqr < range * range * RangeMultiplierInWalls)
{
if (!entitiesInRange.Contains(character)) { entitiesInRange.Add(character); }
charactersInRange.Add((character, nodes.First()));
}
}
}
@@ -378,7 +388,7 @@ namespace Barotrauma.Items.Components
}
else if (entitiesInRange[i] is Character character)
{
dist = Vector2.Distance(character.WorldPosition, currPos);
dist = MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition);
}
if (dist < closestDist)
@@ -494,17 +504,31 @@ namespace Barotrauma.Items.Components
if (IgnoreUser && character == user) { continue; }
if (OutdoorsOnly && character.Submarine != null) { continue; }
Vector2 characterMin = new Vector2(character.AnimController.Limbs.Min(l => l.WorldPosition.X), character.AnimController.Limbs.Min(l => l.WorldPosition.Y));
Vector2 characterMax = new Vector2(character.AnimController.Limbs.Max(l => l.WorldPosition.X), character.AnimController.Limbs.Max(l => l.WorldPosition.Y));
if (targetStructure.IsHorizontal)
{
if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) { continue; }
if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) { continue; }
if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) { continue; }
if (characterMax.X < targetStructure.WorldRect.X) { continue; }
if (characterMin.X > targetStructure.WorldRect.Right) { continue; }
if (Math.Abs(characterMin.Y - targetStructure.WorldPosition.Y) > currentRange &&
Math.Abs(characterMax.Y - targetStructure.WorldPosition.Y) > currentRange)
{
continue;
}
}
else
{
if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; }
if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) { continue; }
if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) { continue; }
if (characterMax.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; }
if (characterMin.Y > targetStructure.WorldRect.Y) { continue; }
if (Math.Abs(characterMin.X - targetStructure.WorldPosition.X) > currentRange &&
Math.Abs(characterMax.X - targetStructure.WorldPosition.X) > currentRange)
{
continue;
}
}
if (!charactersInRange.Any(c => c.character == character))
{
charactersInRange.Add((character, nodes[parentNodeIndex]));
}
float closestNodeDistSqr = float.MaxValue;
int closestNodeIndex = -1;
@@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components
{
public enum UseEnvironment
{
Air, Water, Both
Air, Water, Both, None
};
private float useState;
@@ -44,6 +44,7 @@ namespace Barotrauma.Items.Components
{
if (character == null || character.Removed) { return false; }
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; }
if (UsableIn == UseEnvironment.None) { return false; }
IsActive = true;
useState = 0.1f;
@@ -476,6 +476,7 @@ namespace Barotrauma.Items.Components
{
foreach (Item item in Inventory.AllItemsMod)
{
item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter);
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter);
item.GetComponent<GeneticMaterial>()?.Equip(ownerCharacter);
autoInjectCooldown = AutoInjectInterval;
@@ -450,6 +450,7 @@ namespace Barotrauma.Items.Components
public override bool Select(Character activator)
{
if (activator == null || activator.Removed) { return false; }
if (Item.Condition <= 0.0f && !UpdateWhenInactive) { return false; }
if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater ||
UsableIn == UseEnvironment.Air && activator.AnimController.InWater)
@@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components
{
private ImmutableDictionary<uint, FabricationRecipe> fabricationRecipes; //this is not readonly because tutorials fuck this up!!!!
private const int MaxAmountToFabricate = 99;
private FabricationRecipe fabricatedItem;
private float timeUntilReady;
private float requiredTime;
@@ -39,6 +41,16 @@ namespace Barotrauma.Items.Components
[Serialize(1.0f, IsPropertySaveable.Yes)]
public float SkillRequirementMultiplier { get; set; }
private int amountToFabricate;
[Serialize(1, IsPropertySaveable.Yes)]
public int AmountToFabricate
{
get { return amountToFabricate; }
set { amountToFabricate = MathHelper.Clamp(value, 1, MaxAmountToFabricate); }
}
private int amountRemaining;
private const float TinkeringSpeedIncrease = 2.5f;
private enum FabricatorState
@@ -183,16 +195,20 @@ namespace Barotrauma.Items.Components
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
#if CLIENT
itemList.Enabled = false;
activateButton.Text = TextManager.Get("FabricatorCancel");
#endif
IsActive = true;
this.user = user;
fabricatedItem = selectedItem;
RefreshAvailableIngredients();
#if CLIENT
itemList.Enabled = false;
if (amountInput != null)
{
amountInput.Enabled = false;
}
RefreshActivateButtonText();
#endif
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
if (!isClient)
{
@@ -249,10 +265,11 @@ namespace Barotrauma.Items.Components
}
#elif CLIENT
itemList.Enabled = true;
if (activateButton != null)
if (amountInput != null)
{
activateButton.Text = TextManager.Get(CreateButtonText);
amountInput.Enabled = true;
}
RefreshActivateButtonText();
#endif
fabricatedItem = null;
}
@@ -518,20 +535,16 @@ namespace Barotrauma.Items.Components
}
}
//disabled "continuous fabrication" for now
//before we enable it, there should be some UI controls for fabricating a specific number of items
/*var prevFabricatedItem = fabricatedItem;
var prevFabricatedItem = fabricatedItem;
var prevUser = user;
CancelFabricating();
if (CanBeFabricated(prevFabricatedItem))
amountRemaining--;
if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser))
{
//keep fabricating if we can fabricate more
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
}*/
CancelFabricating();
}
}
}
@@ -594,7 +607,15 @@ namespace Barotrauma.Items.Components
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; }
if (fabricableItem.RequiresRecipe)
{
if (character == null) { return false; }
if (!character.HasRecipeForItem(fabricableItem.TargetItem.Identifier) &&
GameSession.GetSessionCrewCharacters(CharacterType.Bot).None(c => c.HasRecipeForItem(fabricableItem.TargetItem.Identifier)))
{
return false;
}
}
if (fabricableItem.RequiredMoney > 0)
{
@@ -151,7 +151,6 @@ namespace Barotrauma.Items.Components
set
{
bool changed = currentMode != value;
currentMode = value;
#if CLIENT
if (changed) { prevPassivePingRadius = float.MaxValue; }
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -9,14 +10,6 @@ namespace Barotrauma.Items.Components
{
public const int MaxQuality = 3;
public static readonly float[] QualityCommonnesses = new float[]
{
0.8f,
0.15f,
0.045f,
0.005f,
};
public enum StatType
{
Condition,
@@ -81,5 +74,29 @@ namespace Barotrauma.Items.Components
if (!statValues.ContainsKey(statType)) { return 0.0f; }
return statValues[statType] * qualityLevel;
}
/// <summary>
/// Get a random quality for an item spawning in some sub, taking into account the type of the submarine and the difficulty of the current level
/// (high-quality items become more common as difficulty increases)
/// </summary>
public static int GetSpawnedItemQuality(Submarine submarine, Level level, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
{
if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; }
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f);
return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync);
static float GetCommonness(int quality, float difficultyFactor)
{
return quality switch
{
0 => 1,
1 => MathHelper.Lerp(0.0f, 1f, difficultyFactor),
2 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.15f, 0f)), //15 difficulty transition to next biome - unlock Excellent loot
3 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.35f, 0f)), //35 difficulty transition to next biome - unlock Masterwork loot
_ => 0.0f,
};
}
}
}
}
@@ -1617,11 +1617,7 @@ namespace Barotrauma
public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null)
{
if (effect.intervalTimer > 0.0f)
{
effect.intervalTimer -= deltaTime;
return;
}
if (effect.ShouldWaitForInterval(this, deltaTime)) { return; }
if (!isNetworkEvent && checkCondition)
{
if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; }
@@ -2758,7 +2754,6 @@ namespace Barotrauma
return;
}
#endif
bool remove = false;
foreach (ItemComponent ic in components)
{
@@ -2788,7 +2783,6 @@ namespace Barotrauma
{
var abilityItem = new AbilityApplyTreatment(user, character, this);
user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem);
}
if (remove) { Spawner?.AddItemToRemoveQueue(this); }
@@ -15,10 +15,7 @@ namespace Barotrauma
}
[NetworkSerialize]
public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct
{
public override int GetHashCode() => HashCode.Combine(TalentIdentifier, CharacterID, Stat);
}
public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct;
private readonly Dictionary<TalentStatIdentifier, float> talentStats = new();
@@ -3943,31 +3943,11 @@ namespace Barotrauma
Submarine outpost = null;
if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null)
{
if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null)
if (LevelData.OutpostGenerationParamsExist)
{
Location location = i == 0 ? StartLocation : EndLocation;
OutpostGenerationParams outpostGenerationParams = null;
if (LevelData.ForceOutpostGenerationParams != null)
{
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
}
else
{
var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any());
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.OutpostParams;
}
}
outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.ServerAndClient);
}
OutpostGenerationParams outpostGenerationParams = LevelData.ForceOutpostGenerationParams ??
LevelData.GetSuitableOutpostGenerationParams(location).GetRandom(Rand.RandSync.ServerAndClient);
LocationType locationType = location?.Type;
if (locationType == null)
{
@@ -57,10 +57,19 @@ namespace Barotrauma
/// </summary>
public int? MinMainPathWidth;
/// <summary>
/// Events that have previously triggered in this level. Used for making events the player hasn't seen yet more likely to trigger when re-entering the level. Has a maximum size of <see cref="EventManager.MaxEventHistory"/>.
/// </summary>
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
public readonly HashSet<Identifier> UsedUniqueSets = new HashSet<Identifier>();
/// <summary>
/// Events that have already triggered in this level and can never trigger again. <see cref="EventSet.OncePerLevel"/>.
/// </summary>
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
/// <summary>
/// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
/// </summary>
public bool EventsExhausted { get; set; }
/// <summary>
@@ -144,8 +153,6 @@ namespace Barotrauma
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { });
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)));
UsedUniqueSets = element.GetAttributeIdentifierArray(nameof(UsedUniqueSets), Array.Empty<Identifier>()).ToHashSet();
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
}
@@ -245,6 +252,23 @@ namespace Barotrauma
return levelData;
}
public bool OutpostGenerationParamsExist => ForceOutpostGenerationParams != null || OutpostGenerationParams.OutpostParams.Any();
public static IEnumerable<OutpostGenerationParams> GetSuitableOutpostGenerationParams(Location location)
{
var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any());
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.OutpostParams;
}
}
return suitableParams;
}
public void Save(XElement parentElement)
{
var newElement = new XElement("Level",
@@ -287,11 +311,6 @@ namespace Barotrauma
}
}
if (UsedUniqueSets.Any())
{
newElement.Add(new XAttribute(nameof(UsedUniqueSets), string.Join(',', UsedUniqueSets)));
}
parentElement.Add(newElement);
}
}
@@ -1291,19 +1291,32 @@ namespace Barotrauma
return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
}
public int HighestSubmarineTierAvailable(SubmarineClass submarineClass)
public bool CanHaveSubsForSale()
{
if (!HasOutpost()) { return 0; }
return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier;
return HasOutpost() && CanHaveCampaignInteraction(CampaignMode.InteractionType.PurchaseSub);
}
public int HighestSubmarineTierAvailable() => HighestSubmarineTierAvailable(SubmarineClass.Undefined);
public int HighestSubmarineTierAvailable(SubmarineClass submarineClass = SubmarineClass.Undefined)
{
if (CanHaveSubsForSale())
{
return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier;
}
return 0;
}
public bool IsSubmarineAvailable(SubmarineInfo info)
{
return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true;
}
private bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
{
return LevelData != null &&
LevelData.OutpostGenerationParamsExist &&
LevelData.GetSuitableOutpostGenerationParams(this).Any(p => p.CanHaveCampaignInteraction(interactionType));
}
public void Reset()
{
if (Type != OriginalType)
@@ -85,9 +85,9 @@ namespace Barotrauma
}
public float StoreMaxReputationModifier { get; } = 0.1f;
public float StoreSellPriceModifier { get; } = 0.8f;
public float StoreSellPriceModifier { get; } = 0.3f;
public float DailySpecialPriceModifier { get; } = 0.5f;
public float RequestGoodPriceModifier { get; } = 1.5f;
public float RequestGoodPriceModifier { get; } = 2f;
public int StoreInitialBalance { get; } = 5000;
/// <summary>
/// In percentages
@@ -260,6 +260,21 @@ namespace Barotrauma
return humanPrefabCollections.GetRandom(randSync);
}
public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
{
foreach (var collection in humanPrefabCollections)
{
foreach (var prefab in collection)
{
if (prefab.CampaignInteractionType == interactionType)
{
return true;
}
}
}
return false;
}
public ImmutableHashSet<Identifier> GetStoreIdentifiers()
{
if (StoreIdentifiers == null)
@@ -164,7 +164,11 @@ namespace Barotrauma.Networking
return command;
}
public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionmult = 2.0f)
/// <summary>
/// How much messages sent by <paramref name="sender"/> should get garbled. Takes the distance between the entities and optionally the obstructions between them into account (see <paramref name="obstructionMultiplier"/>).
/// </summary>
/// <param name="obstructionMultiplier">Values greater than or equal to 1 cause the message to get garbled more heavily when there's some obstruction between the characters. Values smaller than 1 mean the garbling only depends on distance.</param>
public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionMultiplier = 2.0f)
{
if (listener == null || sender == null)
{
@@ -177,12 +181,12 @@ namespace Barotrauma.Networking
Hull listenerHull = listener == null ? null : Hull.FindHull(listener.WorldPosition);
Hull sourceHull = sender == null ? null : Hull.FindHull(sender.WorldPosition);
if (sourceHull != listenerHull)
if (sourceHull != listenerHull && obstructionMultiplier >= 1.0f)
{
if ((sourceHull == null || !sourceHull.GetConnectedHulls(includingThis: false, searchDepth: 2, ignoreClosedGaps: true).Contains(listenerHull)) &&
Submarine.CheckVisibility(listener.SimPosition, sender.SimPosition) != null)
{
dist = (dist + 100f) * obstructionmult;
dist = (dist + 100f) * obstructionMultiplier;
}
}
if (dist > range) { return 1.0f; }
@@ -197,9 +201,9 @@ namespace Barotrauma.Networking
return ApplyDistanceEffect(listener, Sender, Text, SpeakRange);
}
public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionmult = 2.0f)
public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionMultiplier = 2.0f)
{
return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionmult));
return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionMultiplier));
}
public static string ApplyDistanceEffect(string text, float garbleAmount)
@@ -252,7 +256,7 @@ namespace Barotrauma.Networking
var senderRadio = senderItem.GetComponent<WifiComponent>();
if (!receiverRadio.CanReceive(senderRadio)) { continue; }
string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range);
string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range, obstructionMultiplier: 0);
if (sender.SpeechImpediment > 0.0f)
{
//speech impediment doesn't reduce the range when using a radio, but adds extra garbling
@@ -17,14 +17,9 @@ namespace Barotrauma.Networking
public LidgrenAddress(IPAddress netAddress)
{
if (IPAddress.IsLoopback(netAddress))
{
NetAddress = IPAddress.Loopback;
}
else
{
NetAddress = netAddress;
}
if (IPAddress.IsLoopback(netAddress)) { netAddress = IPAddress.Loopback; }
if (netAddress.IsIPv4MappedToIPv6) { netAddress = netAddress.MapToIPv4(); }
NetAddress = netAddress;
}
public new static Option<LidgrenAddress> Parse(string endpointStr)
@@ -19,7 +19,7 @@ namespace Barotrauma.Networking
public LidgrenEndpoint(IPEndPoint netEndpoint) : base(new LidgrenAddress(netEndpoint.Address))
{
NetEndpoint = netEndpoint;
NetEndpoint = new IPEndPoint((Address as LidgrenAddress)!.NetAddress, netEndpoint.Port);
}
public new static Option<LidgrenEndpoint> Parse(string endpointStr)
@@ -95,11 +95,7 @@ namespace Barotrauma
public override void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
{
if (this.type != type) { return; }
if (intervalTimer > 0.0f)
{
intervalTimer -= deltaTime;
return;
}
if (ShouldWaitForInterval(entity, deltaTime)) { return; }
if (!HasRequiredItems(entity)) { return; }
if (delayType == DelayTypes.ReachCursor && Character.Controlled == null) { return; }
if (!Stackable)
@@ -416,22 +416,26 @@ namespace Barotrauma
return false;
}
switch (Operator)
{
case OperatorType.Equals:
if (type == typeof(bool))
{
return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True");
if (type == typeof(bool))
{
return property.GetBoolValue(target) == (AttributeValue == "true" || AttributeValue == "True");
}
var value = property.GetValue(target);
return Equals(value, AttributeValue);
}
return property.GetValue(target).ToString().Equals(AttributeValue);
case OperatorType.NotEquals:
if (type == typeof(bool))
{
return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True");
if (type == typeof(bool))
{
return property.GetBoolValue(target) != (AttributeValue == "true" || AttributeValue == "True");
}
var value = property.GetValue(target);
return !Equals(value, AttributeValue);
}
return !property.GetValue(target).ToString().Equals(AttributeValue);
case OperatorType.GreaterThan:
case OperatorType.LessThanEquals:
case OperatorType.LessThan:
@@ -441,6 +445,18 @@ namespace Barotrauma
break;
}
return false;
static bool Equals(object value, string desiredValue)
{
if (value == null)
{
return desiredValue.Equals("null", StringComparison.OrdinalIgnoreCase);
}
else
{
return value.ToString().Equals(desiredValue);
}
}
}
}
@@ -343,7 +343,7 @@ namespace Barotrauma
private readonly float lifeTime;
private float lifeTimer;
public float intervalTimer;
public Dictionary<Entity, float> intervalTimers = new Dictionary<Entity, float>();
public static readonly List<DurationListElement> DurationList = new List<DurationListElement>();
@@ -1120,6 +1120,26 @@ namespace Barotrauma
}
}
private static readonly List<Entity> intervalsToRemove = new List<Entity>();
public bool ShouldWaitForInterval(Entity entity, float deltaTime)
{
if (Interval > 0.0f && entity != null)
{
if (intervalTimers.ContainsKey(entity))
{
intervalTimers[entity] -= deltaTime;
if (intervalTimers[entity] > 0.0f) { return true; }
}
intervalsToRemove.Clear();
intervalsToRemove.AddRange(intervalTimers.Keys.Where(e => e.Removed));
foreach (var toRemove in intervalsToRemove)
{
intervalTimers.Remove(toRemove);
}
}
return false;
}
public virtual void Apply(ActionType type, float deltaTime, Entity entity, ISerializableEntity target, Vector2? worldPosition = null)
{
if (this.type != type || !HasRequiredItems(entity)) { return; }
@@ -1147,12 +1167,7 @@ namespace Barotrauma
public virtual void Apply(ActionType type, float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Vector2? worldPosition = null)
{
if (this.type != type) { return; }
if (intervalTimer > 0.0f)
{
intervalTimer -= deltaTime;
return;
}
if (ShouldWaitForInterval(entity, deltaTime)) { return; }
currentTargets.Clear();
foreach (ISerializableEntity target in targets)
@@ -1255,11 +1270,8 @@ namespace Barotrauma
lifeTimer -= deltaTime;
if (lifeTimer <= 0) { return; }
}
if (intervalTimer > 0.0f)
{
intervalTimer -= deltaTime;
return;
}
if (ShouldWaitForInterval(entity, deltaTime)) { return; }
Hull hull = GetHull(entity);
Vector2 position = GetPosition(entity, targets, worldPosition);
if (useItemCount > 0)
@@ -1942,7 +1954,10 @@ namespace Barotrauma
ApplyProjSpecific(deltaTime, entity, targets, hull, position, playSound: true);
intervalTimer = Interval;
if (Interval > 0.0f && entity != null)
{
intervalTimers[entity] = Interval;
}
static Character CharacterFromTarget(ISerializableEntity target)
{
@@ -1,12 +1,12 @@
#nullable enable
using Barotrauma.IO;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Globalization;
using System.Text.Unicode;
@@ -33,54 +33,104 @@ namespace Barotrauma
public static int LanguageVersion { get; private set; } = 0;
private static readonly ImmutableArray<Range<int>> CjkRanges = new[]
{
UnicodeRanges.HangulJamo,
UnicodeRanges.Hiragana,
UnicodeRanges.Katakana,
UnicodeRanges.CjkRadicalsSupplement,
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.EnclosedCjkLettersandMonths,
UnicodeRanges.CjkCompatibility,
UnicodeRanges.CjkUnifiedIdeographsExtensionA,
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.HangulSyllables,
UnicodeRanges.CjkCompatibilityForms
}.Select(r => new Range<int>(r.FirstCodePoint, r.FirstCodePoint+r.Length-1))
.OrderBy(r => r.Start)
.ToImmutableArray();
private static ImmutableArray<Range<int>> UnicodeToIntRanges(params UnicodeRange[] ranges)
=> ranges
.Select(r => new Range<int>(r.FirstCodePoint, r.FirstCodePoint + r.Length - 1))
.OrderBy(r => r.Start)
.ToImmutableArray();
/// <summary>
/// Does the string contain symbols from Chinese, Japanese or Korean languages
/// </summary>
public static bool IsCJK(LocalizedString text)
[Flags]
public enum SpeciallyHandledCharCategory
{
return IsCJK(text.Value);
None = 0x0,
CJK = 0x1,
Cyrillic = 0x2,
All = 0x3
}
public static bool IsCJK(string text)
{
if (string.IsNullOrEmpty(text)) { return false; }
public static readonly ImmutableArray<SpeciallyHandledCharCategory> SpeciallyHandledCharCategories
= Enum.GetValues<SpeciallyHandledCharCategory>()
.Where(c => c is not (SpeciallyHandledCharCategory.None or SpeciallyHandledCharCategory.All))
.ToImmutableArray();
private static readonly ImmutableDictionary<SpeciallyHandledCharCategory, ImmutableArray<Range<int>>> SpeciallyHandledCharacterRanges
= new[]
{
(SpeciallyHandledCharCategory.CJK, UnicodeToIntRanges(
UnicodeRanges.HangulJamo,
UnicodeRanges.Hiragana,
UnicodeRanges.Katakana,
UnicodeRanges.CjkRadicalsSupplement,
UnicodeRanges.CjkSymbolsandPunctuation,
UnicodeRanges.EnclosedCjkLettersandMonths,
UnicodeRanges.CjkCompatibility,
UnicodeRanges.CjkUnifiedIdeographsExtensionA,
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.HangulSyllables,
UnicodeRanges.CjkCompatibilityForms
)),
(SpeciallyHandledCharCategory.Cyrillic, UnicodeToIntRanges(
UnicodeRanges.Cyrillic,
UnicodeRanges.CyrillicSupplement,
UnicodeRanges.CyrillicExtendedA,
UnicodeRanges.CyrillicExtendedB,
UnicodeRanges.CyrillicExtendedC
))
}.ToImmutableDictionary();
public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(LocalizedString text)
=> GetSpeciallyHandledCategories(text.Value);
public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(string text)
{
if (string.IsNullOrEmpty(text)) { return SpeciallyHandledCharCategory.None; }
var retVal = SpeciallyHandledCharCategory.None;
for (int i = 0; i < text.Length; i++)
{
char chr = text[i];
for (int j = 0; j < CjkRanges.Length; j++)
foreach (var category in SpeciallyHandledCharCategories)
{
var range = CjkRanges[j];
if (retVal.HasFlag(category)) { continue; }
for (int j = 0; j < SpeciallyHandledCharacterRanges[category].Length; j++)
{
var range = SpeciallyHandledCharacterRanges[category][j];
// If chr < range.Start, we know that it can't
// be in any of the following ranges, so let's
// not even bother checking them
if (chr < range.Start) { break; }
// If chr < range.Start, we know that it can't
// be in any of the following ranges, so let's
// not even bother checking them
if (chr < range.Start) { break; }
// This character is in a range, return true
if (range.Contains(chr)) { return true; }
// This character is in a range, set the flag
if (range.Contains(chr))
{
retVal |= category;
break;
}
}
}
if (retVal == SpeciallyHandledCharCategory.All)
{
// Input contains characters from all
// specially handled categories, there's
// no need to inspect the string further
return SpeciallyHandledCharCategory.All;
}
}
return false;
return retVal;
}
public static bool IsCJK(LocalizedString text)
=> IsCJK(text.Value);
public static bool IsCJK(string text)
=> GetSpeciallyHandledCategories(text).HasFlag(SpeciallyHandledCharCategory.CJK);
/// <summary>
/// Check if the currently selected language is available, and switch to English if not
/// </summary>
@@ -562,35 +562,45 @@ namespace Barotrauma
public static double LineSegmentToPointDistanceSquared(Point lineA, Point lineB, Point point)
{
double xDiff = lineB.X - lineA.X;
double yDiff = lineB.Y - lineA.Y;
return LineSegmentToPointDistanceSquared(lineA.X, lineA.Y, lineB.X, lineB.Y, point.X, point.Y);
}
public static float LineSegmentToPointDistanceSquared(Vector2 lineA, Vector2 lineB, Vector2 point)
{
return (float)LineSegmentToPointDistanceSquared(lineA.X, lineA.Y, lineB.X, lineB.Y, point.X, point.Y);
}
private static double LineSegmentToPointDistanceSquared(double line1X, double line1Y, double line2X, double line2Y, double pointX, double pointY)
{
double xDiff = line2X - line1X;
double yDiff = line2Y - line1Y;
if (xDiff == 0 && yDiff == 0)
{
double v1 = lineA.X - point.X;
double v2 = lineA.Y - point.Y;
double v1 = line1X - pointX;
double v2 = line1Y - pointY;
return (v1 * v1) + (v2 * v2);
}
// Calculate the t that minimizes the distance.
double t = ((point.X - lineA.X) * xDiff + (point.Y - lineA.Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff);
double t = ((pointX - line1X) * xDiff + (pointY - line1Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff);
// See if this represents one of the segment's
// end points or a point in the middle.
if (t < 0)
{
xDiff = point.X - lineA.X;
yDiff = point.Y - lineA.Y;
xDiff = pointX - line1X;
yDiff = pointY - line1Y;
}
else if (t > 1)
{
xDiff = point.X - lineB.X;
yDiff = point.Y - lineB.Y;
xDiff = pointX - line2X;
yDiff = pointY - line2Y;
}
else
{
xDiff = point.X - (lineA.X + t * xDiff);
yDiff = point.Y - (lineA.Y + t * yDiff);
xDiff = pointX - (line1X + t * xDiff);
yDiff = pointY - (line1Y + t * yDiff);
}
return xDiff * xDiff + yDiff * yDiff;
@@ -7,6 +7,7 @@ using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
@@ -825,16 +826,29 @@ namespace Barotrauma
public static bool StatIdentifierMatches(Identifier original, Identifier match)
{
if (original == match) { return true; }
return Matches(original, match) || Matches(match, original);
for (int i = 0; i < match.Value.Length; i++)
static bool Matches(Identifier a, Identifier b)
{
if (i >= original.Value.Length) { return match[i] is '~'; }
if (!CharEquals(original[i], match[i])) { return false; }
for (int i = 0; i < b.Value.Length; i++)
{
if (i >= a.Value.Length) { return b[i] is '~'; }
if (!CharEquals(a[i], b[i])) { return false; }
}
return false;
}
return false;
static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b);
}
public static bool EquivalentTo(this IPEndPoint self, IPEndPoint other)
=> self.Address.EquivalentTo(other.Address) && self.Port == other.Port;
public static bool EquivalentTo(this IPAddress self, IPAddress other)
{
if (self.IsIPv4MappedToIPv6) { self = self.MapToIPv4(); }
if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); }
return self.Equals(other);
}
}
}
+44
View File
@@ -1,3 +1,47 @@
---------------------------------------------------------------------------------------------------------
v0.20.7.0
---------------------------------------------------------------------------------------------------------
Changes:
- Added a slider to the fabricator that can be used to select how many items to fabricate.
- Chance of finding good/excellent/masterwork quality items in higher-difficulty levels.
- Changed engines' default repair threshold to 50 to 80.
- Quality of the radio voice chat diminishes with distance (gradually fading into radio static), similar to the way as the quality of the text chat.
- Voice chat range also affects spectators (= spectators can't hear players talking at the other side of the level).
Unstable only:
- Changed how the restrictions on specialization talent trees work: instead of being only allowed to choose one, you can unlock all of them (but need to complete the entire specialization tree before you can choose another one).
- Any number of generic talents can be chosen (but you can still only pick talents from the next tier when you've unlocked 2 generic talents).
- Anyone can use the recipes bots have unlocked via talents.
- Various fixes, balance changes and improvements to talents.
- Fixed Arc Emitter damaging nearby characters that aren't near the arc (e.g. characters behind the shooter's back).
- Fixed null reference exception in EnemyAIController.UpdateLimbAttack (caused frequent crashes when getting attacked by swarms).
- Improvements to the exosuit sprite.
- Fixed bots not being able to apply medical treatment anymore.
- Fixed switching from a directional ping to passive immediately making the item's AITarget sector a full 360 degrees, and AITarget's sector not being taken into account when determining what it should reveal on passive sonar.
- Fixed submarine availability being displayed incorrectly on some locations (e.g. abandoned outposts) on the campaign map.
- Fixed feign death not working when you have a provocative items (e.g. items that make sounds or otherwise attrack monsters) in the inventory.
- Restored some of the wrecked sprites that were replaced with tinted version of the normal sprite in the previous builds (WIP).
- Fixed fiber plant sprite.
- Fixed bigbrother event getting stuck when choosing "not interested".
- Fixed autoinjectors (autoinjector headset, PUCS) no longer working.
Bugfixes:
- Fixed main menu sometimes appearing half obstructed when starting the game on Mac.
- Fixed speech impediments only affecting the text chat, not the voice chat.
- Fixed radio voice chat not working properly if the range of the radio is larger than 250 meters. Happened because characters' positions aren't synced if they're further away than 250 meters from the client. In practice, the quality/volume of the chat would stop diminishing after 250 meters, and then immediately cut off when outside the radio range.
- Fixed inability to connect to IPv4 servers when IPv6 is disabled.
- Fixed husk appendage breaking if the character already has extra limbs from e.g. genes.
- Fixed blocked doorways in Alien_Entrance3.
- Fixed Cyrillic symbols not being visible in the server list's server info panel when playing in a language other than Russian.
- Fixed certain genetic effects (such as regeneration from Hammerhead Matriarch genes) not working properly when multiple characters have the same effect.
- Adjusted railgun, coilgun and double coilgun firing offsets to make the projectile spawn closer to the end of the barrel.
- Fixed incorrect tags in the vending machine in EngineeringModule_01_Colony, causing engineering gear to spawn in the output slot.
Modding:
- Made it possible to check if some value is null or not with PropertyConditionals (e.g. CurrentHull="eq null").
- Added UseEnvironment.None to Propulsion component.
---------------------------------------------------------------------------------------------------------
v0.20.6.0
---------------------------------------------------------------------------------------------------------