2f107db...5202af9

This commit is contained in:
Joonas Rikkonen
2019-03-18 21:42:26 +02:00
parent 58c92888b7
commit 044fd3344b
395 changed files with 27417 additions and 19754 deletions
@@ -12,7 +12,17 @@ namespace Barotrauma
private AIState state;
protected AITarget selectedAiTarget;
protected AITarget _previousAiTarget;
protected AITarget _selectedAiTarget;
public AITarget SelectedAiTarget
{
get { return _selectedAiTarget; }
protected set
{
_previousAiTarget = _selectedAiTarget;
_selectedAiTarget = value;
}
}
protected SteeringManager steeringManager;
@@ -57,11 +67,6 @@ namespace Barotrauma
get { return null; }
}
public AITarget SelectedAiTarget
{
get { return selectedAiTarget; }
}
public AIState State
{
get { return state; }
@@ -27,7 +27,7 @@ namespace Barotrauma
public float SightRange
{
get { return sightRange; }
set { sightRange = MathHelper.Clamp(value, MinSightRange, MaxSoundRange); }
set { sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange); }
}
private float sectorRad = MathHelper.TwoPi;
@@ -58,8 +58,8 @@ namespace Barotrauma
public bool Enabled = true;
public readonly float MinSoundRange, MinSightRange;
public readonly float MaxSoundRange = float.MaxValue, MaxSightRange = float.MaxValue;
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = float.MaxValue, MaxSightRange = float.MaxValue;
public Vector2 WorldPosition
{
@@ -101,19 +101,12 @@ namespace Barotrauma
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = MinSightRange = element.GetAttributeFloat("sightrange", 0.0f);
SoundRange = MinSoundRange = element.GetAttributeFloat("soundrange", 0.0f);
// Use the min and max definitions if found.
if (element.Attribute("minsightrange") != null)
{
MinSightRange = element.GetAttributeFloat("minsightrange", MinSightRange);
}
if (element.Attribute("minsoundrange") != null)
{
MinSoundRange = element.GetAttributeFloat("minsoundrange", MinSoundRange);
}
MaxSightRange = element.GetAttributeFloat("maxsightrange", MaxSightRange);
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", MaxSoundRange);
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
SoundRange = element.GetAttributeFloat("soundrange", 0.0f);
MinSightRange = element.GetAttributeFloat("minsightrange", SightRange);
MinSoundRange = element.GetAttributeFloat("minsoundrange", SoundRange);
MaxSightRange = element.GetAttributeFloat("maxsightrange", SightRange);
MaxSoundRange = element.GetAttributeFloat("maxsoundrange", SoundRange);
SonarLabel = element.GetAttributeString("sonarlabel", "");
}
@@ -88,9 +88,7 @@ namespace Barotrauma
private AITargetMemory selectedTargetMemory;
private float targetValue;
private float eatTimer;
private Dictionary<AITarget, AITargetMemory> targetMemories;
//the eyesight of the NPC (0.0 = blind, 1.0 = sees every target within sightRange)
@@ -250,7 +248,7 @@ namespace Barotrauma
public override void SelectTarget(AITarget target)
{
selectedAiTarget = target;
SelectedAiTarget = target;
selectedTargetMemory = FindTargetMemory(target);
targetValue = 100.0f;
@@ -292,7 +290,7 @@ namespace Barotrauma
UpdateTargets(Character, out targetingPriority);
updateTargetsTimer = UpdateTargetsInterval;
if (selectedAiTarget == null)
if (SelectedAiTarget == null)
{
State = AIState.Idle;
}
@@ -308,7 +306,7 @@ namespace Barotrauma
latchOntoAI?.Update(this, deltaTime);
if (selectedAiTarget != null && (selectedAiTarget.Entity == null || selectedAiTarget.Entity.Removed))
if (SelectedAiTarget != null && (SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed))
{
State = AIState.Idle;
return;
@@ -368,9 +366,9 @@ namespace Barotrauma
if (wallTarget != null) return;
if (selectedAiTarget != null)
if (SelectedAiTarget != null)
{
Vector2 targetSimPos = Character.Submarine == null ? ConvertUnits.ToSimUnits(selectedAiTarget.WorldPosition) : selectedAiTarget.SimPosition;
Vector2 targetSimPos = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition;
steeringManager.SteeringAvoid(deltaTime, colliderSize * 3.0f, speed);
steeringManager.SteeringSeek(targetSimPos, speed);
@@ -392,13 +390,13 @@ namespace Barotrauma
private void UpdateEscape(float deltaTime)
{
if (selectedAiTarget == null || selectedAiTarget.Entity == null || selectedAiTarget.Entity.Removed)
if (SelectedAiTarget == null || SelectedAiTarget.Entity == null || SelectedAiTarget.Entity.Removed)
{
State = AIState.Idle;
return;
}
Vector2 escapeDir = Vector2.Normalize(SimPosition - selectedAiTarget.SimPosition);
Vector2 escapeDir = Vector2.Normalize(SimPosition - SelectedAiTarget.SimPosition);
if (!MathUtils.IsValid(escapeDir)) escapeDir = Vector2.UnitY;
float speed = Character.AnimController.GetCurrentSpeed(useMaxSpeed: true);
SteeringManager.SteeringManual(deltaTime, escapeDir * speed);
@@ -415,7 +413,7 @@ namespace Barotrauma
private void UpdateAttack(float deltaTime)
{
if (selectedAiTarget == null)
if (SelectedAiTarget == null)
{
State = AIState.Idle;
return;
@@ -425,23 +423,23 @@ namespace Barotrauma
selectedTargetMemory.Priority -= deltaTime * 0.1f;
Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(selectedAiTarget.WorldPosition) : selectedAiTarget.SimPosition;
Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition;
if (Character.Submarine != null && selectedAiTarget.Entity.Submarine != null && Character.Submarine != selectedAiTarget.Entity.Submarine)
if (Character.Submarine != null && SelectedAiTarget.Entity.Submarine != null && Character.Submarine != SelectedAiTarget.Entity.Submarine)
{
attackSimPosition = ConvertUnits.ToSimUnits(selectedAiTarget.WorldPosition - Character.Submarine.Position);
attackSimPosition = ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition - Character.Submarine.Position);
}
if (selectedAiTarget.Entity is Item item)
if (SelectedAiTarget.Entity is Item item)
{
// If the item is held by a character, attack the character instead.
var pickable = item.components.Select(c => c as Pickable).FirstOrDefault();
var pickable = item.GetComponent<Pickable>();
if (pickable != null)
{
var target = pickable.Picker?.AiTarget;
if (target != null)
{
selectedAiTarget = target;
SelectedAiTarget = target;
}
}
}
@@ -449,13 +447,13 @@ namespace Barotrauma
if (wallTarget != null)
{
attackSimPosition = ConvertUnits.ToSimUnits(wallTarget.Position);
if (Character.Submarine == null && selectedAiTarget.Entity?.Submarine != null) attackSimPosition += ConvertUnits.ToSimUnits(selectedAiTarget.Entity.Submarine.Position);
if (Character.Submarine == null && SelectedAiTarget.Entity?.Submarine != null) attackSimPosition += ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position);
}
else if (selectedAiTarget.Entity is Character)
else if (SelectedAiTarget.Entity is Character)
{
//target the closest limb if the target is a character
float closestDist = Vector2.DistanceSquared(selectedAiTarget.SimPosition, SimPosition) * 10.0f;
foreach (Limb limb in ((Character)selectedAiTarget.Entity).AnimController.Limbs)
float closestDist = Vector2.DistanceSquared(SelectedAiTarget.SimPosition, SimPosition) * 10.0f;
foreach (Limb limb in ((Character)SelectedAiTarget.Entity).AnimController.Limbs)
{
if (limb == null) continue;
float dist = Vector2.DistanceSquared(limb.SimPosition, SimPosition) / Math.Max(limb.AttackPriority, 0.1f);
@@ -514,9 +512,9 @@ namespace Barotrauma
return;
}
}
else if (selectedAiTarget.Entity is Item)
else if (SelectedAiTarget.Entity is Item)
{
var door = ((Item)selectedAiTarget.Entity).GetComponent<Door>();
var door = ((Item)SelectedAiTarget.Entity).GetComponent<Door>();
//steer through the door manually if it's open or broken
if (door?.LinkedGap?.FlowTargetHull != null && !door.LinkedGap.IsRoomToRoom && (door.IsOpen || door.Item.Condition <= 0.0f))
{
@@ -566,24 +564,39 @@ namespace Barotrauma
{
if (attackingLimb.attack.SecondaryCoolDownTimer <= 0)
{
// If the secondary cooldown is defined and expired, check if we can switch the attack
var previousLimb = attackingLimb;
var newLimb = GetAttackLimb(attackSimPosition, previousLimb);
if (newLimb != null)
// Don't allow attacking when the attack target has changed.
if (_previousAiTarget != null && SelectedAiTarget != _previousAiTarget)
{
attackingLimb = newLimb;
canAttack = false;
if (attackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.PursueIfCanAttack)
{
// Fall back if cannot attack.
UpdateFallBack(attackSimPosition, deltaTime);
return;
}
attackingLimb = null;
}
else
{
// No new limb was found.
if (attackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
// If the secondary cooldown is defined and expired, check if we can switch the attack
var previousLimb = attackingLimb;
var newLimb = GetAttackLimb(attackSimPosition, previousLimb);
if (newLimb != null)
{
canAttack = false;
attackingLimb = newLimb;
}
else
{
UpdateFallBack(attackSimPosition, deltaTime);
return;
// No new limb was found.
if (attackingLimb.attack.AfterAttack == AIBehaviorAfterAttack.Pursue)
{
canAttack = false;
}
else
{
UpdateFallBack(attackSimPosition, deltaTime);
return;
}
}
}
}
@@ -648,12 +661,12 @@ namespace Barotrauma
else if (indoorsSteering.CurrentPath.CurrentNode?.ConnectedDoor != null)
{
wallTarget = null;
selectedAiTarget = indoorsSteering.CurrentPath.CurrentNode.ConnectedDoor.Item.AiTarget;
SelectedAiTarget = indoorsSteering.CurrentPath.CurrentNode.ConnectedDoor.Item.AiTarget;
}
else if (indoorsSteering.CurrentPath.NextNode?.ConnectedDoor != null)
{
wallTarget = null;
selectedAiTarget = indoorsSteering.CurrentPath.NextNode.ConnectedDoor.Item.AiTarget;
SelectedAiTarget = indoorsSteering.CurrentPath.NextNode.ConnectedDoor.Item.AiTarget;
}
}
}
@@ -668,7 +681,7 @@ namespace Barotrauma
private Limb GetAttackLimb(Vector2 attackSimPosition, Limb ignoredLimb = null)
{
AttackContext currentContext = Character.GetAttackContext();
var target = wallTarget != null ? wallTarget.Structure : selectedAiTarget.Entity;
var target = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity;
var limbs = Character.AnimController.Limbs
.Where(l =>
l != ignoredLimb &&
@@ -677,7 +690,7 @@ namespace Barotrauma
!l.IsStuck &&
l.attack.IsValidContext(currentContext) &&
l.attack.IsValidTarget(target) &&
l.attack.Conditionals.All(c => (target is ISerializableEntity se && c.Matches(se)) || !(target is ISerializableEntity)))
l.attack.Conditionals.All(c => (target is ISerializableEntity se && c.Matches(se)) || !(target is ISerializableEntity) || !(target is Character)))
.OrderByDescending(l => l.attack.Priority)
.ThenBy(l => ConvertUnits.ToDisplayUnits(Vector2.Distance(l.SimPosition, attackSimPosition)));
// TODO: priority should probably not override the distance -> use values instead of booleans
@@ -695,11 +708,11 @@ namespace Barotrauma
//check if there's a wall between the target and the Character
Vector2 rayStart = Character.SimPosition;
Vector2 rayEnd = selectedAiTarget.SimPosition;
Vector2 rayEnd = SelectedAiTarget.SimPosition;
if (selectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
if (SelectedAiTarget.Entity.Submarine != null && Character.Submarine == null)
{
rayStart -= ConvertUnits.ToSimUnits(selectedAiTarget.Entity.Submarine.Position);
rayStart -= ConvertUnits.ToSimUnits(SelectedAiTarget.Entity.Submarine.Position);
}
Body closestBody = Submarine.CheckVisibility(rayStart, rayEnd);
@@ -795,7 +808,7 @@ namespace Barotrauma
private void UpdateLimbAttack(float deltaTime, Limb limb, Vector2 attackPosition, float distance = -1)
{
var damageTarget = wallTarget != null ? wallTarget.Structure : selectedAiTarget.Entity as IDamageable;
var damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
if (damageTarget == null) return;
float prevHealth = damageTarget.Health;
@@ -834,7 +847,7 @@ namespace Barotrauma
private void UpdateEating(float deltaTime)
{
if (selectedAiTarget == null)
if (SelectedAiTarget == null)
{
State = AIState.Idle;
return;
@@ -850,13 +863,13 @@ namespace Barotrauma
}
Vector2 mouthPos = Character.AnimController.GetMouthPosition().Value;
Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(selectedAiTarget.WorldPosition) : selectedAiTarget.SimPosition;
Vector2 attackSimPosition = Character.Submarine == null ? ConvertUnits.ToSimUnits(SelectedAiTarget.WorldPosition) : SelectedAiTarget.SimPosition;
Vector2 limbDiff = attackSimPosition - mouthPos;
float limbDist = limbDiff.Length();
if (limbDist < 2.0f)
{
Character.SelectCharacter(selectedAiTarget.Entity as Character);
Character.SelectCharacter(SelectedAiTarget.Entity as Character);
steeringManager.SteeringManual(deltaTime, limbDiff);
Character.AnimController.Collider.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f, mouthPos);
}
@@ -875,10 +888,8 @@ namespace Barotrauma
//sight/hearing range
public void UpdateTargets(Character character, out TargetingPriority targetingPriority)
{
var prevAiTarget = selectedAiTarget;
targetingPriority = null;
selectedAiTarget = null;
SelectedAiTarget = null;
selectedTargetMemory = null;
targetValue = 0.0f;
@@ -936,11 +947,9 @@ namespace Barotrauma
//skip the target if it's a room and the character is already inside a sub
if (character.CurrentHull != null && target.Entity is Hull) continue;
//multiply the priority of the target if it's a door from outside to inside and the AI is an aggressive boarder
Door door = null;
if (target.Entity is Item item)
{
//item inside and we're outside -> attack the hull
if (item.CurrentHull != null && character.CurrentHull == null)
{
@@ -963,21 +972,20 @@ namespace Barotrauma
}
if (door != null)
{
{
//increase priority if the character is outside and an aggressive boarder, and the door is from outside to inside
if (character.CurrentHull == null && aggressiveBoarding && !door.LinkedGap.IsRoomToRoom)
{
valueModifier = door.IsOpen ? 5.0f : 3.0f;
valueModifier = door.IsOpen ? 10 : 5;
}
else if (door.IsOpen || door.Item.Condition <= 0.0f) //ignore broken and open doors
{
continue;
}
}
else
else if (target.Entity is IDamageable targetDamageable && targetDamageable.Health <= 0.0f)
{
IDamageable targetDamageable = target.Entity as IDamageable;
if (targetDamageable != null && targetDamageable.Health <= 0.0f) continue;
continue;
}
}
@@ -988,7 +996,8 @@ namespace Barotrauma
if (valueModifier == 0.0f) continue;
dist = Vector2.Distance(character.WorldPosition, target.WorldPosition);
Vector2 toTarget = target.WorldPosition - character.WorldPosition;
dist = toTarget.Length();
//if the target has been within range earlier, the character will notice it more easily
//(i.e. remember where the target was)
@@ -1003,21 +1012,27 @@ namespace Barotrauma
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = FindTargetMemory(target);
if (Character.CurrentHull != null && Math.Abs(toTarget.Y) > Character.CurrentHull.Size.Y)
{
// Inside the sub, treat objects that are up or down, as they were farther away.
dist *= 3;
}
valueModifier = valueModifier * targetMemory.Priority / (float)Math.Sqrt(dist);
if (valueModifier > targetValue)
{
selectedAiTarget = target;
SelectedAiTarget = target;
selectedTargetMemory = targetMemory;
targetingPriority = targetingPriorities[targetingTag];
targetValue = valueModifier;
}
}
if (selectedAiTarget != prevAiTarget)
if (SelectedAiTarget != _previousAiTarget)
{
wallTarget = null;
}
}
_previousAiTarget = SelectedAiTarget;
}
//find the targetMemory that corresponds to some AItarget or create if there isn't one yet
@@ -57,7 +57,7 @@ namespace Barotrauma
{
if (DisableCrewAI || Character.IsUnconscious) return;
if (Character.Submarine != null || selectedAiTarget?.Entity?.Submarine != null)
if (Character.Submarine != null || SelectedAiTarget?.Entity?.Submarine != null)
{
if (steeringManager != insideSteering) insideSteering.Reset();
steeringManager = insideSteering;
@@ -122,9 +122,11 @@ namespace Barotrauma
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
if (run || Character.SpeedMultiplier <= 0.0f) targetMovement *= Character.SpeedMultiplier;
Character.SpeedMultiplier = 1.0f; // Reset, items will set the value before the next update
float speedMultiplier = Character.SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
Character.ResetSpeedMultiplier(); // Reset, items will set the value before the next update
Character.AnimController.TargetMovement = targetMovement;
}
@@ -225,6 +227,8 @@ namespace Barotrauma
}
}
partial void ReportProblems();
private void UpdateSpeaking()
{
if (Character.Oxygen < 20.0f)
@@ -277,7 +281,23 @@ namespace Barotrauma
public override void SelectTarget(AITarget target)
{
selectedAiTarget = target;
SelectedAiTarget = target;
}
private void CheckCrouching(float deltaTime)
{
crouchRaycastTimer -= deltaTime;
if (crouchRaycastTimer > 0.0f) return;
crouchRaycastTimer = CrouchRaycastInterval;
//start the raycast in front of the character in the direction it's heading to
Vector2 startPos = Character.SimPosition;
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;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
}
private void CheckCrouching(float deltaTime)
@@ -18,6 +18,10 @@ namespace Barotrauma
private float findPathTimer;
private float buttonPressCooldown;
const float ButtonPressInterval = 0.5f;
public SteeringPath CurrentPath
{
get { return currentPath; }
@@ -57,6 +61,7 @@ namespace Barotrauma
{
base.Update(speed);
buttonPressCooldown -= 1.0f / 60.0f;
findPathTimer -= 1.0f / 60.0f;
}
@@ -127,7 +132,7 @@ namespace Barotrauma
return currentTarget - pos2;
}
if (canOpenDoors && !character.LockHands)
if (canOpenDoors && !character.LockHands && buttonPressCooldown <= 0.0f)
{
CheckDoorsInPath();
}
@@ -311,6 +316,7 @@ namespace Barotrauma
}
closestButton.Item.TryInteract(character, false, true, false);
buttonPressCooldown = ButtonPressInterval;
break;
}
}
@@ -236,7 +236,11 @@ namespace Barotrauma
foreach (Character potentialSpeaker in availableSpeakers)
{
//check if the character has an appropriate job to say the line
if (selectedConversation.AllowedJobs.Count > 0 && !selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) continue;
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) ||
selectedConversation.AllowedJobs.Count > 0)
{
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) continue;
}
//check if the character has all required flags to say the line
if (!ignoreFlags)
@@ -310,6 +314,81 @@ namespace Barotrauma
return 1.0f - 1.0f / (index + 1);
}
#if DEBUG
public static void WriteToCSV()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < list.Count; i++)
{
NPCConversation current = list[i];
WriteConversation(sb, current, 0);
WriteSubConversations(sb, current.Responses, 1);
WriteEmptyRow(sb);
}
StreamWriter file = new StreamWriter(@"NPCConversations.csv");
file.WriteLine(sb.ToString());
file.Close();
}
private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex)
{
sb.Append(conv.speakerIndex); // Speaker index
sb.Append('*');
sb.Append(depthIndex); // Depth index
sb.Append('*');
sb.Append(conv.Line); // Original
sb.Append('*');
// Translated
sb.Append('*');
sb.Append(string.Join(",", conv.Flags)); // Flags
sb.Append('*');
for (int i = 0; i < conv.AllowedJobs.Count; i++) // Jobs
{
sb.Append(conv.AllowedJobs[i].Identifier);
if (i < conv.AllowedJobs.Count - 1)
{
sb.Append(",");
}
}
sb.Append('*');
sb.Append(string.Join(",", conv.allowedSpeakerTags)); // Traits
sb.Append('*');
sb.Append(conv.minIntensity); // Minimum intensity
sb.Append('*');
sb.Append(conv.maxIntensity); // Maximum intensity
sb.Append('*');
// Comments
sb.AppendLine();
}
private static void WriteSubConversations(System.Text.StringBuilder sb, List<NPCConversation> responses, int depthIndex)
{
for (int i = 0; i < responses.Count; i++)
{
WriteConversation(sb, responses[i], depthIndex);
if (responses[i].Responses != null && responses[i].Responses.Count > 0)
{
WriteSubConversations(sb, responses[i].Responses, depthIndex + 1);
}
}
}
private static void WriteEmptyRow(System.Text.StringBuilder sb)
{
for (int i = 0; i < 8; i++)
{
sb.Append('*');
}
sb.AppendLine();
}
#endif
}
}
@@ -60,10 +60,10 @@ namespace Barotrauma
weapon.GetComponent<RepairTool>() as ItemComponent;
if (weaponComponent != null && weaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{
Item[] containedItems = weapon.ContainedItems;
var containedItems = weapon.ContainedItems;
foreach (RelatedItem requiredItem in weaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
Item containedItem = Array.Find(containedItems, it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it));
Item containedItem = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (containedItem == null)
{
var newReloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, weapon.GetComponent<ItemContainer>());
@@ -1,7 +1,6 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -1,5 +1,6 @@
using Barotrauma.Items.Components;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -19,7 +20,7 @@ namespace Barotrauma
var containedItems = character.Inventory.Items[i].ContainedItems;
if (containedItems == null) continue;
var oxygenTank = Array.Find(containedItems, it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
var oxygenTank = containedItems.FirstOrDefault(it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
if (oxygenTank != null) return true;
}
}
@@ -2,6 +2,7 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -60,7 +61,7 @@ namespace Barotrauma
var containedItems = weldingTool.ContainedItems;
if (containedItems == null) return;
var fuelTank = Array.Find(containedItems, i => i.HasTag("weldingfueltank") && i.Condition > 0.0f);
var fuelTank = containedItems.FirstOrDefault(i => i.HasTag("weldingfueltank") && i.Condition > 0.0f);
if (fuelTank == null)
{
AddSubObjective(new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>()));
@@ -108,16 +108,20 @@ namespace Barotrauma
//not linked to a hull -> ignore
if (gap.linkedTo.All(l => l == null)) { continue; }
if (character.TeamID == 0)
if (character.TeamID == Character.TeamType.None)
{
//TODO: make sure the gap isn't in another sub (outpost, respawn shuttle...?)
if (gap.Submarine == null) continue;
}
else
{
//prevent characters from attempting to fix leaks in the enemy sub
//team 1 plays in sub 0, team 2 in sub 1
Submarine mySub = character.TeamID < 1 || character.TeamID > Submarine.MainSubs.Length ?
Submarine.MainSub : Submarine.MainSubs[character.TeamID - 1];
Submarine mySub = Submarine.MainSub;
if (character.TeamID == Character.TeamType.Team2 && Submarine.MainSubs.Length > 1)
{
mySub = Submarine.MainSubs[1];
}
if (gap.Submarine != mySub) continue;
}
@@ -24,8 +24,9 @@ namespace Barotrauma
public override float GetPriority(AIObjectiveManager objectiveManager)
{
if (Target != null && Target.Removed) return 0.0f;
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) return 0.0f;
if (FollowControlledCharacter && Character.Controlled == null) { return 0.0f; }
if (Target != null && Target.Removed) { return 0.0f; }
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) { return 0.0f; }
if (objectiveManager.CurrentOrder == this)
{
@@ -39,15 +40,17 @@ namespace Barotrauma
{
get
{
if (Target != null && Target.Removed) return false;
if (FollowControlledCharacter && Character.Controlled == null) { return false; }
if (repeat || waitUntilPathUnreachable > 0.0f) return true;
if (Target != null && Target.Removed) { return false; }
if (repeat || waitUntilPathUnreachable > 0.0f) { return true; }
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
//path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable
if (pathSteering?.CurrentPath == null) return true;
if (pathSteering?.CurrentPath == null) { return true; }
if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) return false;
if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) { return false; }
return !pathSteering.CurrentPath.Unreachable;
}
@@ -55,6 +58,8 @@ namespace Barotrauma
public Entity Target { get; private set; }
public bool FollowControlledCharacter;
public AIObjectiveGoTo(Entity target, Character character, bool repeat = false, bool getDivingGearIfNeeded = true)
: base (character, "")
{
@@ -78,12 +83,18 @@ namespace Barotrauma
protected override void Act(float deltaTime)
{
if (FollowControlledCharacter)
{
if (Character.Controlled == null) { return; }
Target = Character.Controlled;
}
if (Target == character)
{
character.AIController.SteeringManager.Reset();
return;
}
waitUntilPathUnreachable -= deltaTime;
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
@@ -132,7 +132,8 @@ namespace Barotrauma
{
CloseEnough = 1.5f,
AllowGoingOutside = true,
IgnoreIfTargetDead = true
IgnoreIfTargetDead = true,
FollowControlledCharacter = orderGiver == character
};
break;
case "wait":
@@ -28,7 +28,7 @@ namespace Barotrauma
if (!repairablesFound) { return 0.0f; }
float priority = 100.0f - item.Condition;
float priority = item.Prefab.Health - item.Condition;
//vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
float dist =
Math.Abs(character.WorldPosition.X - item.WorldPosition.X) +
@@ -48,7 +48,7 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
//ignore items that are in full condition
if (item.Condition >= 100.0f) continue;
if (item.Condition >= item.Prefab.Health) continue;
foreach (Repairable repairable in item.Repairables)
{
//ignore ones that are already fixed
@@ -194,7 +194,7 @@ namespace Barotrauma
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
bool remove = false;
foreach (ItemComponent ic in item.components)
foreach (ItemComponent ic in item.Components)
{
if (!ic.HasRequiredContainedItems(addMessage: false)) continue;
#if CLIENT
@@ -161,16 +161,16 @@ namespace Barotrauma
return false;
}
public string GetChatMessage(string targetCharacterName, string targetRoomName, string orderOption = "")
public string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, string orderOption = "")
{
orderOption = orderOption ?? "";
string messageTag = "OrderDialog." + AITag;
string messageTag = (givingOrderToSelf && !TargetAllCharacters ? "OrderDialogSelf." : "OrderDialog.") + AITag;
if (!string.IsNullOrEmpty(orderOption)) messageTag += "." + orderOption;
string msg = TextManager.Get(messageTag, true);
if (msg == null) return "";
if (targetCharacterName == null) targetCharacterName = "";
if (targetRoomName == null) targetRoomName = "";
return msg.Replace("[name]", targetCharacterName).Replace("[roomname]", targetRoomName);
@@ -23,6 +23,7 @@ namespace Barotrauma
public AICharacter(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
: base(file, position, seed, characterInfo, isNetworkPlayer, ragdoll)
{
InitProjSpecific();
}
partial void InitProjSpecific();
@@ -50,12 +51,13 @@ namespace Barotrauma
if (!IsRemotePlayer)
{
float characterDist = Vector2.DistanceSquared(cam.WorldViewCenter, WorldPosition);
#if SERVER
if (GameMain.Server != null)
{
//get the distance from the closest player to this character
foreach (Character c in CharacterList)
{
if (c != this && (c.IsRemotePlayer || c == GameMain.Server.Character))
if (c != this && c.IsRemotePlayer)
{
float dist = Vector2.DistanceSquared(c.WorldPosition, WorldPosition);
if (dist < characterDist)
@@ -66,6 +68,7 @@ namespace Barotrauma
}
}
}
#endif
if (characterDist > EnableSimplePhysicsDistSqr)
{
@@ -78,15 +81,14 @@ namespace Barotrauma
}
if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return;
if (Controlled == this || !aiController.Enabled) return;
if (!aiController.Enabled) return;
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) return;
if (Controlled == this) return;
SoundUpdate(deltaTime);
if (!IsRemotePlayer)
{
aiController.Update(deltaTime);
}
}
partial void SoundUpdate(float deltaTime);
}
}
@@ -131,22 +131,22 @@ namespace Barotrauma
{
if (Frozen) return;
if (MainLimb == null) { return; }
if (character.IsDead || character.IsUnconscious || character.Stun > 0.0f)
if (!character.AllowInput)
{
Collider.Enabled = false;
levitatingCollider = false;
Collider.FarseerBody.FixedRotation = false;
//set linear velocity even though the collider is disabled,
//because the character won't be able to switch back from ragdoll mode until the velocity of the collider is low enough
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.FarseerBody.FixedRotation = false;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
}
if (character.IsDead && deathAnimTimer < deathAnimDuration)
{
deathAnimTimer += deltaTime;
UpdateDying(deltaTime);
}
}
return;
}
else
@@ -266,6 +266,11 @@ namespace Barotrauma
}
}
private bool CanDrag(Character target)
{
return Mass / target.Mass > 0.1f;
}
private float eatTimer = 0.0f;
public override void DragCharacter(Character target, float deltaTime)
@@ -294,9 +299,12 @@ namespace Barotrauma
{
//pull the target character to the position of the mouth
//(+ make the force fluctuate to waggle the character a bit)
targetCharacter.AnimController.MainLimb.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + 10.0f));
targetCharacter.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation);
targetCharacter.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + 10.0f));
if (CanDrag(target))
{
targetCharacter.AnimController.MainLimb.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + 10.0f));
targetCharacter.AnimController.MainLimb.body.SmoothRotate(mouthLimb.Rotation, 20.0f);
targetCharacter.AnimController.Collider.MoveToPos(mouthPos, (float)(Math.Sin(eatTimer) + 10.0f));
}
//pull the character's mouth to the target character (again with a fluctuating force)
float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f));
@@ -496,7 +504,7 @@ namespace Barotrauma
Vector2 colliderBottom = GetColliderBottom();
float movementAngle = 0.0f;
float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle.Value : HeadAngle.Value) * Dir;
float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle ?? 0 : HeadAngle ?? 0) * Dir;
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
{
movementAngle += MathHelper.TwoPi;
@@ -669,6 +677,8 @@ namespace Barotrauma
limb.body.ApplyForce(diff * (float)(Math.Sin(WalkPos) * Math.Sqrt(limb.Mass)) * 30.0f * animStrength);
}
limb?.body.SmoothRotate(angle, torque, wrapAngle: false);
}
private void SmoothRotateWithoutWrapping(Limb limb, float angle, Limb referenceLimb, float torque)
@@ -707,8 +717,17 @@ namespace Barotrauma
centerOfMass,
new Vector2(centerOfMass.X - (l.SimPosition.X - centerOfMass.X), l.SimPosition.Y),
true);
l.body.PositionSmoothingFactor = 0.8f;
}
}
if (character.SelectedCharacter != null && CanDrag(character.SelectedCharacter))
{
float diff = character.SelectedCharacter.SimPosition.X - centerOfMass.X;
if (diff < 100.0f)
{
character.SelectedCharacter.AnimController.SetPosition(
new Vector2(centerOfMass.X - diff, character.SelectedCharacter.SimPosition.Y), lerp: true);
}
}
}
}
}
@@ -341,10 +341,13 @@ namespace Barotrauma
if (!character.AllowInput)
{
levitatingCollider = false;
Collider.Enabled = false;
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.FarseerBody.FixedRotation = false;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Collider.Enabled = false;
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
}
return;
}
@@ -498,7 +501,7 @@ namespace Barotrauma
}
aiming = false;
if (character.IsRemotePlayer && GameMain.Server == null) Collider.LinearVelocity = Vector2.Zero;
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) return;
}
void UpdateStanding()
@@ -569,8 +572,11 @@ namespace Barotrauma
movement.Y = 0.0f;
if (torso == null) { return; }
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (onGround && (!character.IsRemotePlayer || GameMain.Server != null))
if (onGround && isNotRemote)
{
//move slower if collider isn't upright
float rotationFactor = (float)Math.Abs(Math.Cos(Collider.Rotation));
@@ -783,7 +789,7 @@ namespace Barotrauma
{
Collider.LinearVelocity = movement;
}
else if (onGround && (!character.IsRemotePlayer || GameMain.Server != null))
else if (onGround && (!character.IsRemotePlayer || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)))
{
Collider.LinearVelocity = new Vector2(
movement.X,
@@ -976,7 +982,10 @@ namespace Barotrauma
movement.Y = movement.Y - (surfaceLimiter - 1.0f) * 0.01f;
}
if (!character.IsRemotePlayer || GameMain.Server != null)
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (isNotRemote)
{
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, movementLerp);
}
@@ -1165,7 +1174,11 @@ namespace Barotrauma
trigger = character.SelectedConstruction.TransformTrigger(trigger);
bool notClimbing = false;
if (character.IsRemotePlayer && GameMain.Server == null)
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (isNotRemote)
{
notClimbing = character.IsKeyDown(InputType.Left) || character.IsKeyDown(InputType.Right);
}
@@ -1263,9 +1276,9 @@ namespace Barotrauma
bool wasCritical = target.Vitality < 0.0f;
if (GameMain.Client == null) //Serverside code
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
{
target.Oxygen += deltaTime * 0.5f; //Stabilize them
target.Oxygen += deltaTime * 0.5f; //Stabilize them
}
int skill = (int)character.GetSkillLevel("medical");
@@ -1279,15 +1292,18 @@ namespace Barotrauma
torso.PullJointEnabled = true;
//Serverside code
if (GameMain.Client == null && target.Oxygen < -10.0f)
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.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
if (target.Oxygen < -10.0f)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.StabilizationMin, CPRSettings.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
//DebugConsole.NewMessage("CPR Us: " + character.Oxygen + " Them: " + target.Oxygen + " How good we are: restore " + cpr + " use " + (30.0f - cpr), Color.Aqua);
//DebugConsole.NewMessage("CPR Us: " + character.Oxygen + " Them: " + target.Oxygen + " How good we are: restore " + cpr + " use " + (30.0f - cpr), Color.Aqua);
}
}
}
else
@@ -1319,7 +1335,7 @@ namespace Barotrauma
},
0.0f, true, 0.0f, character);
}
if (GameMain.Client == null) //Serverside code
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
{
float reviveChance = skill * CPRSettings.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.ReviveChanceExponent);
@@ -1330,7 +1346,7 @@ namespace Barotrauma
//increase oxygen and clamp it above zero
// -> the character should be revived if there are no major afflictions in addition to lack of oxygen
target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f);
}
}
}
}
cprPump += deltaTime;
@@ -1380,7 +1396,7 @@ namespace Barotrauma
if (Anim == Animation.Climbing)
{
//cannot drag up ladders if the character is conscious
if (target.AllowInput && GameMain.Client == null)
if (target.AllowInput && (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient))
{
character.DeselectCharacter();
return;
@@ -1459,7 +1475,7 @@ namespace Barotrauma
Limb pullLimb = i == 0 ? leftHand : rightHand;
if (GameMain.Client == null)
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
//stop dragging if there's something between the pull limb and the target limb
Vector2 sourceSimPos = pullLimb.SimPosition;
@@ -1541,7 +1557,7 @@ namespace Barotrauma
float dist = ConvertUnits.ToSimUnits(Vector2.Distance(target.WorldPosition, WorldPosition));
//let the target break free if it's moving away and gets far enough
if (GameMain.Client == null && dist > 1.4f && target.AllowInput &&
if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && dist > 1.4f && target.AllowInput &&
Vector2.Dot(target.WorldPosition - WorldPosition, target.AnimController.TargetMovement) > 0)
{
character.DeselectCharacter();
@@ -1609,7 +1625,7 @@ namespace Barotrauma
if (Anim != Animation.Climbing && !usingController && character.Stun <= 0.0f && aim && itemPos != Vector2.Zero)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
Vector2 diff = holdable.Aimable ? (mousePos - AimSourceSimPos) * Dir : Vector2.UnitX;
@@ -1872,15 +1888,16 @@ namespace Barotrauma
for (int i = 0; i < character.SelectedItems.Length; i++)
{
if (character.SelectedItems[i] != null && character.SelectedItems[i].body != null)
if (i == 1 && character.SelectedItems[0] == character.SelectedItems[1])
{
break;
}
if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed)
{
difference = character.SelectedItems[i].body.SimPosition - torso.SimPosition;
difference = Vector2.Transform(difference, torsoTransform);
difference.Y = -difference.Y;
character.SelectedItems[i].body.SetTransform(
torso.SimPosition + Vector2.Transform(difference, -torsoTransform),
MathUtils.WrapAngleTwoPi(-character.SelectedItems[i].body.Rotation));
character.SelectedItems[i].body.SimPosition,
MathUtils.WrapAngleTwoPi(character.SelectedItems[i].body.Rotation + MathHelper.Pi));
character.SelectedItems[i].GetComponent<Holdable>()?.Flip();
}
}
@@ -1898,7 +1915,6 @@ namespace Barotrauma
case LimbType.RightHand:
case LimbType.RightArm:
case LimbType.RightForearm:
mirror = true;
flipAngle = true;
break;
case LimbType.LeftThigh:
@@ -0,0 +1,150 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace Barotrauma
{
partial class HumanoidAnimParams : ISerializableEntity
{
public string Name
{
get;
private set;
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
public HumanoidAnimParams(string file)
{
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null || doc.Root == null) return;
Name = doc.Root.Name.ToString();
SerializableProperties = SerializableProperty.DeserializeProperties(this, doc.Root);
}
[Serialize(0.3f, true), Editable]
public float GetUpSpeed
{
get;
set;
}
[Serialize(1.54f, true), Editable]
public float HeadPosition
{
get;
set;
}
[Serialize(1.15f, true), Editable]
public float TorsoPosition
{
get;
set;
}
[Serialize(0.25f, true), Editable]
public float HeadLeanAmount
{
get;
set;
}
[Serialize(0.25f, true), Editable]
public float TorsoLeanAmount
{
get;
set;
}
[Serialize(5.0f, true), Editable]
public float CycleSpeed
{
get;
set;
}
[Serialize(15.0f, true), Editable]
public float FootMoveStrength
{
get;
set;
}
[Serialize(20.0f, true), Editable]
public float FootRotateStrength
{
get;
set;
}
[Serialize("0.4,0.12", true), Editable]
public Vector2 StepSize
{
get;
set;
}
[Serialize("0.0, 0.0", true), Editable]
public Vector2 FootMoveOffset
{
get;
set;
}
[Serialize(10.0f, true), Editable]
public float LegCorrectionTorque
{
get;
set;
}
[Serialize(15.0f, true), Editable]
public float ThighCorrectionTorque
{
get;
set;
}
[Serialize("0.4, 0.15", true), Editable]
public Vector2 HandMoveAmount
{
get;
set;
}
[Serialize("-0.15, 0.0", true), Editable]
public Vector2 HandMoveOffset
{
get;
set;
}
[Serialize(0.7f, true), Editable]
public float HandMoveStrength
{
get;
set;
}
[Serialize(-1.0f, true), Editable]
public float HandClampY
{
get;
set;
}
}
}
@@ -97,7 +97,7 @@ namespace Barotrauma
public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType.ToString()}";
public static string GetDefaultFolder(string speciesName) => $"Content/Characters/{speciesName.CapitaliseFirstInvariant()}/Animations/";
public static string GetDefaultFile(string speciesName, AnimationType animType) => $"{GetDefaultFolder(speciesName)}{GetDefaultFileName(speciesName, animType)}.xml";
public static string GetDefaultFile(string speciesName, AnimationType animType) => $"{GetFolder(speciesName)}{GetDefaultFileName(speciesName, animType)}.xml";
protected static string GetFolder(string speciesName)
{
@@ -64,7 +64,7 @@ namespace Barotrauma
public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll";
public static string GetDefaultFolder(string speciesName) => $"Content/Characters/{speciesName.CapitaliseFirstInvariant()}/Ragdolls/";
public static string GetDefaultFile(string speciesName) => $"{GetDefaultFolder(speciesName)}{GetDefaultFileName(speciesName)}.xml";
public static string GetDefaultFile(string speciesName) => $"{GetFolder(speciesName)}{GetDefaultFileName(speciesName)}.xml";
private static readonly object[] dummyParams = new object[]
{
@@ -662,7 +662,7 @@ namespace Barotrauma
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
public virtual void AddToEditor(ParamsEditor editor)
{
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true);
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, inGame: false, showName: true);
SubParams.ForEach(sp => sp.AddToEditor(editor));
}
#endif
@@ -314,16 +314,16 @@ namespace Barotrauma
{
if (!PhysicsBody.IsValidShape(limbParams.Radius, limbParams.Height, limbParams.Width))
{
DebugConsole.ThrowError("Cannot create the ragdoll: invalid collider dimensions on limb: " + limbParams.Name);
return;
DebugConsole.ThrowError($"Invalid collider dimensions (r: {limbParams.Radius}, h: {limbParams.Height}, w: {limbParams.Width}) on limb: {limbParams.Name}. Fixing.");
limbParams.Radius = 10;
}
}
foreach (var colliderParams in RagdollParams.ColliderParams)
{
if (!PhysicsBody.IsValidShape(colliderParams.Radius, colliderParams.Height, colliderParams.Width))
{
DebugConsole.ThrowError("Cannot create the ragdoll: invalid collider dimensions on collider: " + colliderParams.Name);
return;
DebugConsole.ThrowError($"Invalid collider dimensions (r: {colliderParams.Radius}, h: {colliderParams.Height}, w: {colliderParams.Width}) on collider: {colliderParams.Name}. Fixing.");
colliderParams.Radius = 10;
}
}
CreateColliders();
@@ -335,11 +335,21 @@ namespace Barotrauma
{
foreach (var kvp in items)
{
var limb = limbs[kvp.Key.ID];
int id = kvp.Key.ID;
// This can be the case if we manipulate the ragdoll in runtime (husk appendage, limb severance)
if (id > limbs.Length - 1) { continue; }
var limb = limbs[id];
var itemList = kvp.Value;
limb.WearingItems.AddRange(itemList);
}
}
if (character.SpeciesName.ToLowerInvariant() == "humanhusk")
{
if (Limbs.None(l => l.Name.ToLowerInvariant() == "huskappendage"))
{
AfflictionHusk.AttachHuskAppendage(character, this);
}
}
}
public Ragdoll(Character character, string seed, RagdollParams ragdollParams = null)
@@ -431,12 +441,7 @@ namespace Barotrauma
}
partial void SetupDrawOrder();
/// <summary>
/// Inversed draw order, which is used for drawing the limbs in 3d (deformable sprites).
/// </summary>
protected Limb[] inversedLimbDrawOrder;
/// <summary>
/// Saves all serializable data in the currently selected ragdoll params. This method should properly handle character flipping.
/// </summary>
@@ -495,9 +500,7 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
byte limb1ID = Convert.ToByte(jointParams.Limb1);
byte limb2ID = Convert.ToByte(jointParams.Limb2);
LimbJoint joint = new LimbJoint(Limbs[limb1ID], Limbs[limb2ID], jointParams, this);
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
GameMain.World.AddJoint(joint);
for (int i = 0; i < LimbJoints.Length; i++)
{
@@ -509,11 +512,6 @@ namespace Barotrauma
LimbJoints[LimbJoints.Length - 1] = joint;
}
public void AddJoint(XElement subElement, float scale = 1.0f)
{
AddJoint(new JointParams(subElement, RagdollParams));
}
protected void AddLimb(LimbParams limbParams)
{
byte ID = Convert.ToByte(limbParams.ID);
@@ -591,7 +589,6 @@ namespace Barotrauma
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
Structure structure = f2.Body.UserData as Structure;
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) return false;
@@ -599,7 +596,7 @@ namespace Barotrauma
if (f2.Body.UserData as string == "blocker" && f2.Body != outsideCollisionBlocker) return false;
//always collides with bodies other than structures
if (structure == null)
if (!(f2.Body.UserData is Structure structure))
{
CalculateImpact(f1, f2, contact);
return true;
@@ -635,8 +632,7 @@ namespace Barotrauma
if (contact.Manifold.LocalNormal.Y < 0.0f) return false;
//4. contact points is above the bottom half of the collider
Vector2 normal; FarseerPhysics.Common.FixedArray2<Vector2> points;
contact.GetWorldManifold(out normal, out points);
contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) return false;
//5. in water
@@ -671,7 +667,10 @@ namespace Barotrauma
float impact = Vector2.Dot(velocity, -normal);
if (f1.Body == Collider.FarseerBody)
{
if (!character.IsRemotePlayer || GameMain.Server != null)
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (isNotRemote)
{
if (impact > ImpactTolerance)
{
@@ -689,7 +688,7 @@ namespace Barotrauma
ImpactProjSpecific(impact, f1.Body);
}
public void SeverLimbJoint(LimbJoint limbJoint)
{
if (!limbJoint.CanBeSevered || limbJoint.IsSevered)
@@ -713,9 +712,9 @@ namespace Barotrauma
SeverLimbJointProjSpecific(limbJoint);
if (GameMain.Server != null)
if (GameMain.NetworkMember!=null && GameMain.NetworkMember.IsServer)
{
GameMain.Server.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status });
GameMain.NetworkMember.CreateEntityEvent(character, new object[] { NetEntityEvent.Type.Status });
}
}
@@ -985,6 +984,12 @@ namespace Barotrauma
SetPosition(Collider.SimPosition + moveAmount);
character.CursorPosition += moveAmount;
Collider?.UpdateDrawPosition();
foreach (Limb limb in Limbs)
{
limb.body.UpdateDrawPosition();
}
}
private void UpdateCollisionCategories()
@@ -1025,6 +1030,8 @@ namespace Barotrauma
{
if (!character.Enabled || Frozen) return;
CheckValidity();
UpdateNetPlayerPosition(deltaTime);
CheckDistFromCollider();
UpdateCollisionCategories();
@@ -1158,10 +1165,7 @@ namespace Barotrauma
{
if (contacts.Contact.Enabled && contacts.Contact.IsTouching)
{
Vector2 normal;
FarseerPhysics.Common.FixedArray2<Vector2> points;
contacts.Contact.GetWorldManifold(out normal, out points);
contacts.Contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
switch (contacts.Contact.FixtureA.CollisionCategories)
{
@@ -1288,6 +1292,55 @@ namespace Barotrauma
UpdateProjSpecific(deltaTime);
}
private void CheckValidity()
{
CheckValidity(Collider);
foreach (Limb limb in limbs)
{
CheckValidity(limb.body);
}
}
private void CheckValidity(PhysicsBody body)
{
string errorMsg = null;
string bodyName = body.UserData is Limb ? "Limb" : "Collider";
if (!MathUtils.IsValid(body.SimPosition) || Math.Abs(body.SimPosition.X) > 1e10f || Math.Abs(body.SimPosition.Y) > 1e10f)
{
errorMsg = bodyName+ " position invalid (" + body.SimPosition + ", character: " + character.Name + "), resetting the ragdoll.";
}
else if (!MathUtils.IsValid(body.LinearVelocity) || Math.Abs(body.LinearVelocity.X) > 1000f || Math.Abs(body.LinearVelocity.Y) > 1000f)
{
errorMsg = bodyName + " velocity invalid (" + body.LinearVelocity + ", character: " + character.Name + "), resetting the ragdoll.";
}
else if (!MathUtils.IsValid(body.Rotation))
{
errorMsg = bodyName + " rotation invalid (" + body.Rotation + ", character: " + character.Name + "), resetting the ragdoll.";
}
else if (!MathUtils.IsValid(body.AngularVelocity) || Math.Abs(body.AngularVelocity) > 1000f)
{
errorMsg = bodyName + " angular velocity invalid (" + body.AngularVelocity + ", character: " + character.Name + "), resetting the ragdoll.";
}
if (errorMsg != null)
{
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.CheckValidity:" + character.ID, GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
if (!MathUtils.IsValid(Collider.SimPosition) || Math.Abs(Collider.SimPosition.X) > 1e10f || Math.Abs(Collider.SimPosition.Y) > 1e10f)
{
Collider.SetTransform(Vector2.Zero, 0.0f);
}
foreach (Limb limb in Limbs)
{
limb.body.SetTransform(Collider.SimPosition, 0.0f);
limb.body.ResetDynamics();
}
SetInitialLimbPositions();
return;
}
UpdateProjSpecific(deltaTime);
}
partial void UpdateProjSpecific(float deltaTime);
partial void Splash(Limb limb, Hull limbHull);
@@ -1365,7 +1418,15 @@ namespace Barotrauma
Vector2 limbMoveAmount = simPosition - MainLimb.SimPosition;
Collider.SetTransform(simPosition, Collider.Rotation);
if (lerp)
{
Collider.TargetPosition = simPosition;
Collider.MoveToTargetPosition(true);
}
else
{
Collider.SetTransform(simPosition, Collider.Rotation);
}
foreach (Limb limb in Limbs)
{
@@ -1445,7 +1506,8 @@ namespace Barotrauma
prevCollisionCategory = Category.None;
}
}
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
private void UpdateNetPlayerPosition(float deltaTime)
{
if (GameMain.NetworkMember == null) return;
@@ -1467,214 +1529,8 @@ namespace Barotrauma
character.MemState[i].TransformOutToInside(currentHull.Submarine);
}
}
if (GameMain.Server != null) return; //the server should not be trying to correct any positions, it's authoritative
if (character != GameMain.NetworkMember.Character || !character.AllowInput)
{
//remove states without a timestamp (there may still be ID-based states
//in the list when the controlled character switches to timestamp-based interpolation)
character.MemState.RemoveAll(m => m.Timestamp == 0.0f);
//use simple interpolation for other players' characters and characters that can't move
if (character.MemState.Count > 0)
{
CharacterStateInfo serverPos = character.MemState.Last();
if (!character.isSynced)
{
SetPosition(serverPos.Position, false);
Collider.LinearVelocity = Vector2.Zero;
character.MemLocalState.Clear();
character.LastNetworkUpdateID = serverPos.ID;
character.isSynced = true;
return;
}
if (character.MemState[0].Interact == null || character.MemState[0].Interact.Removed)
{
character.DeselectCharacter();
character.SelectedConstruction = null;
}
else if (character.MemState[0].Interact is Character)
{
character.SelectCharacter((Character)character.MemState[0].Interact);
}
else if (character.MemState[0].Interact is Item newSelectedConstruction)
{
if (newSelectedConstruction != null && character.SelectedConstruction != newSelectedConstruction)
{
foreach (var ic in newSelectedConstruction.components)
{
if (ic.CanBeSelected) ic.Select(character);
}
}
character.SelectedConstruction = newSelectedConstruction;
}
if (character.MemState[0].Animation == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
else if (character.AnimController.Anim == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.None;
}
Vector2 newVelocity = Vector2.Zero;
Vector2 newPosition = Collider.SimPosition;
Collider.CorrectPosition(character.MemState, deltaTime, out newVelocity, out newPosition);
newVelocity = newVelocity.ClampLength(100.0f);
if (!MathUtils.IsValid(newVelocity)) newVelocity = Vector2.Zero;
overrideTargetMovement = newVelocity;
Collider.LinearVelocity = newVelocity;
float distSqrd = Vector2.DistanceSquared(newPosition, Collider.SimPosition);
if (distSqrd > 10.0f)
{
SetPosition(newPosition);
}
else if (distSqrd > 0.01f)
{
Collider.SetTransform(newPosition, Collider.Rotation);
}
//unconscious/dead characters can't correct their position using AnimController movement
// -> we need to correct it manually
if (!character.AllowInput)
{
Collider.LinearVelocity = overrideTargetMovement;
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
MainLimb.PullJointEnabled = true;
}
}
character.MemLocalState.Clear();
}
else
{
//remove states with a timestamp (there may still timestamp-based states
//in the list if the controlled character switches from timestamp-based interpolation to ID-based)
character.MemState.RemoveAll(m => m.Timestamp > 0.0f);
for (int i = 0; i < character.MemLocalState.Count; i++)
{
if (character.Submarine == null)
{
//transform in-sub coordinates to outside coordinates
if (character.MemLocalState[i].Position.Y > lowestSubPos)
{
character.MemLocalState[i].TransformInToOutside();
}
}
else if (currentHull?.Submarine != null)
{
//transform outside coordinates to in-sub coordinates
if (character.MemLocalState[i].Position.Y < lowestSubPos)
{
character.MemLocalState[i].TransformOutToInside(currentHull.Submarine);
}
}
}
if (character.MemState.Count < 1) return;
overrideTargetMovement = Vector2.Zero;
CharacterStateInfo serverPos = character.MemState.Last();
if (!character.isSynced)
{
SetPosition(serverPos.Position, false);
Collider.LinearVelocity = Vector2.Zero;
character.MemLocalState.Clear();
character.LastNetworkUpdateID = serverPos.ID;
character.isSynced = true;
return;
}
int localPosIndex = character.MemLocalState.FindIndex(m => m.ID == serverPos.ID);
if (localPosIndex > -1)
{
CharacterStateInfo localPos = character.MemLocalState[localPosIndex];
//the entity we're interacting with doesn't match the server's
if (localPos.Interact != serverPos.Interact)
{
if (serverPos.Interact == null || serverPos.Interact.Removed)
{
character.DeselectCharacter();
character.SelectedConstruction = null;
}
else if (serverPos.Interact is Character)
{
character.SelectCharacter((Character)serverPos.Interact);
}
else
{
var newSelectedConstruction = (Item)serverPos.Interact;
if (newSelectedConstruction != null && character.SelectedConstruction != newSelectedConstruction)
{
newSelectedConstruction.TryInteract(character, true, true);
}
character.SelectedConstruction = newSelectedConstruction;
}
}
if (localPos.Animation != serverPos.Animation)
{
if (serverPos.Animation == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.CPR;
}
else if (character.AnimController.Anim == AnimController.Animation.CPR)
{
character.AnimController.Anim = AnimController.Animation.None;
}
}
Hull serverHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(serverPos.Position), character.CurrentHull, serverPos.Position.Y < lowestSubPos);
Hull clientHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(localPos.Position), serverHull, localPos.Position.Y < lowestSubPos);
if (serverHull != null && clientHull != null && serverHull.Submarine != clientHull.Submarine)
{
//hull subs don't match => teleport the camera to the other sub
character.Submarine = serverHull.Submarine;
character.CurrentHull = currentHull = serverHull;
SetPosition(serverPos.Position);
character.MemLocalState.Clear();
}
else
{
Vector2 positionError = serverPos.Position - localPos.Position;
float rotationError = serverPos.Rotation - localPos.Rotation;
for (int i = localPosIndex; i < character.MemLocalState.Count; i++)
{
Hull pointHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(character.MemLocalState[i].Position), clientHull, character.MemLocalState[i].Position.Y < lowestSubPos);
if (pointHull != clientHull && ((pointHull == null) || (clientHull == null) || (pointHull.Submarine == clientHull.Submarine))) break;
character.MemLocalState[i].Translate(positionError, rotationError);
}
float errorMagnitude = positionError.Length();
if (errorMagnitude > 0.01f)
{
Collider.SetTransform(Collider.SimPosition + positionError, Collider.Rotation + rotationError);
if (errorMagnitude > 0.5f)
{
character.MemLocalState.Clear();
foreach (Limb limb in Limbs)
{
limb.body.SetTransform(limb.body.SimPosition + positionError, limb.body.Rotation);
}
}
}
}
}
if (character.MemLocalState.Count > 120) character.MemLocalState.RemoveRange(0, character.MemLocalState.Count - 120);
character.MemState.Clear();
}
UpdateNetPlayerPositionProjSpecific(deltaTime, lowestSubPos);
}
private Vector2 GetFlowForce()
@@ -1735,11 +1591,8 @@ namespace Barotrauma
{
for (int i = 0; i < Collider.FarseerBody.FixtureList.Count; i++)
{
FarseerPhysics.Collision.AABB aabb;
FarseerPhysics.Common.Transform transform;
Collider.FarseerBody.GetTransform(out transform);
Collider.FarseerBody.FixtureList[i].Shape.ComputeAABB(out aabb, ref transform, i);
Collider.FarseerBody.GetTransform(out FarseerPhysics.Common.Transform transform);
Collider.FarseerBody.FixtureList[i].Shape.ComputeAABB(out FarseerPhysics.Collision.AABB aabb, ref transform, i);
lowestBound = Math.Min(aabb.LowerBound.Y, lowestBound);
}
@@ -356,6 +356,16 @@ namespace Barotrauma
effect.Apply(effectType, deltaTime, (Character)target, ((Character)target).AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
}
if (target is Entity entity)
{
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
effect.GetNearbyTargets(worldPosition, targets);
effect.Apply(ActionType.OnActive, deltaTime, entity, targets);
}
}
}
return attackResult;
@@ -17,6 +17,8 @@ namespace Barotrauma
{
public static List<Character> CharacterList = new List<Character>();
partial void UpdateLimbLightSource(Limb limb);
private bool enabled = true;
public bool Enabled
{
@@ -42,12 +44,7 @@ namespace Barotrauma
{
limb.body.Enabled = enabled;
}
#if CLIENT
if (limb.LightSource != null)
{
limb.LightSource.Enabled = enabled;
}
#endif
UpdateLimbLightSource(limb);
}
AnimController.Collider.Enabled = value;
}
@@ -71,8 +68,16 @@ namespace Barotrauma
protected Key[] keys;
private Item[] selectedItems;
private byte teamID;
public byte TeamID
public enum TeamType
{
None,
Team1,
Team2,
FriendlyNPC
}
private TeamType teamID;
public TeamType TeamID
{
get { return teamID; }
set
@@ -111,6 +116,9 @@ namespace Barotrauma
private string currentOrderOption;
private List<StatusEffect> statusEffects = new List<StatusEffect>();
private List<float> speedMultipliers = new List<float>();
private List<StatusEffect> statusEffects = new List<StatusEffect>();
public Entity ViewTarget
@@ -159,15 +167,22 @@ namespace Barotrauma
return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName;
}
}
private string displayName;
public string DisplayName
{
get
{
return displayName != null && displayName.Length > 0 ? displayName : Name;
}
}
//Only used by server logs to determine "true identity" of the player for cases when they're disguised
public string LogName
{
get
{
if (GameMain.Server != null && !GameMain.Server.AllowDisguises) return Name;
#if CLIENT
if (GameMain.Client != null && !GameMain.Client.AllowDisguises) return Name;
#endif
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises) return Name;
return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName;
}
}
@@ -240,6 +255,12 @@ namespace Barotrauma
}
}
public Vector2 SmoothedCursorPosition
{
get;
private set;
}
public Vector2 CursorWorldPosition
{
get { return Submarine == null ? cursorPosition : cursorPosition + Submarine.Position; }
@@ -345,7 +366,7 @@ namespace Barotrauma
get { return IsRagdolled ? 1.0f : CharacterHealth.StunTimer; }
set
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
SetStun(value, true);
}
@@ -435,16 +456,7 @@ namespace Barotrauma
{
get;
set;
}
/// <summary>
/// Can be used to modify the character's speed via StatusEffects
/// </summary>
public float SpeedMultiplier
{
get;
set;
}
}
public Item[] SelectedItems
{
@@ -517,9 +529,64 @@ namespace Barotrauma
set { canInventoryBeAccessed = value; }
}
private bool canBeDragged = true;
public bool CanBeDragged
{
get
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsDead || Stun > 0.0f || LockHands || IsUnconscious;
}
set { canBeDragged = value; }
}
//can other characters access the inventory of this character
private bool canInventoryBeAccessed = true;
public bool CanInventoryBeAccessed
{
get
{
if (!canInventoryBeAccessed || Removed || Inventory == null) { return false; }
if (!Inventory.AccessibleWhenAlive)
{
return IsDead;
}
else
{
return (IsDead || Stun > 0.0f || LockHands || IsUnconscious);
}
}
set { canInventoryBeAccessed = value; }
}
public override Vector2 SimPosition
{
get { return AnimController.Collider.SimPosition; }
get
{
if (AnimController?.Collider == null)
{
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed+".";
if (AnimController == null)
{
errorMsg += " AnimController == null";
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
return Vector2.Zero;
}
return AnimController.Collider.SimPosition;
}
}
public override Vector2 Position
@@ -550,7 +617,7 @@ namespace Barotrauma
/// <param name="seed">RNG seed to use if the character config has randomizable parameters.</param>
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
/// <param name="hasAi">Is the character controlled by AI.</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select randomly.</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null)
{
return Create(characterInfo.File, position, seed, characterInfo, isRemotePlayer, hasAi, true, ragdoll);
@@ -566,7 +633,7 @@ namespace Barotrauma
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
/// <param name="hasAi">Is the character controlled by AI.</param>
/// <param name="createNetworkEvent">Should clients receive a network event about the creation of this character?</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select randomly.</param>
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
public static Character Create(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null)
{
#if LINUX
@@ -596,7 +663,7 @@ namespace Barotrauma
}
#endif
Character newCharacter = null;
if (file.ToLower() != HumanConfigFile?.ToLower())
if (file != HumanConfigFile)
{
var aiCharacter = new AICharacter(file, position, seed, characterInfo, isRemotePlayer, ragdoll);
var ai = new EnemyAIController(aiCharacter, file, seed);
@@ -622,16 +689,12 @@ namespace Barotrauma
//newCharacter.minVitality = -100.0f;
}
#if SERVER
if (GameMain.Server != null && Spawner != null && createNetworkEvent)
{
Spawner.CreateNetworkEvent(newCharacter, false);
}
if (characterInfo != null)
{
newCharacter.LoadHeadAttachments();
}
#endif
return newCharacter;
}
@@ -654,17 +717,27 @@ namespace Barotrauma
Properties = SerializableProperty.GetProperties(this);
Info = characterInfo;
if (file == humanConfigFile && characterInfo == null)
if (file == HumanConfigFile || file == GetConfigFile("humanhusk"))
{
Info = new CharacterInfo(file);
if (characterInfo == null)
{
Info = new CharacterInfo(HumanConfigFile);
}
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
{
keys[i] = new Key((InputType)i);
}
XDocument doc = XMLExtensions.TryLoadXml(file);
if (doc == null || doc.Root == null) return;
InitProjSpecific(doc);
SpeciesName = doc.Root.GetAttributeString("name", "Unknown");
displayName = TextManager.Get($"Character.{Path.GetFileName(Path.GetDirectoryName(file))}", true);
SpeciesName = doc.Root.GetAttributeString("name", "Unknown");
IsHumanoid = doc.Root.GetAttributeBool("humanoid", false);
canSpeak = doc.Root.GetAttributeBool("canspeak", false);
needsAir = doc.Root.GetAttributeBool("needsair", false);
@@ -744,6 +817,11 @@ namespace Barotrauma
// - server receives an input message from the client controlling the character
// - if an AICharacter, the server enables it when close enough to any of the players
Enabled = GameMain.NetworkMember == null;
if (Info != null)
{
LoadHeadAttachments();
}
}
partial void InitProjSpecific(XDocument doc);
@@ -754,13 +832,7 @@ namespace Barotrauma
if (head == null) { return; }
if (headId.HasValue)
{
Info.Head.HeadSpriteId = headId.Value;
Info.LoadHeadSprite();
Info.HairIndex = hairIndex ?? -1;
Info.BeardIndex = beardIndex ?? -1;
Info.MoustacheIndex = moustacheIndex ?? -1;
Info.FaceAttachmentIndex = faceAttachmentIndex ?? -1;
Info.LoadHeadAttachments();
Info.RecreateHead(headId.Value, Info.Race, Info.Gender, hairIndex ?? -1, beardIndex ?? -1, moustacheIndex ?? -1, faceAttachmentIndex ?? -1);
}
#if CLIENT
head.RecreateSprite();
@@ -816,7 +888,7 @@ namespace Barotrauma
public static string GetConfigFile(string speciesName)
{
string configFile = CharacterConfigFiles.FirstOrDefault(c => Path.GetFileName(c) == $"{speciesName}.xml");
string configFile = CharacterConfigFiles.FirstOrDefault(c => Path.GetFileName(c).ToLowerInvariant() == $"{speciesName.ToLowerInvariant()}.xml");
if (configFile == null)
{
DebugConsole.ThrowError($"Couldn't find a config file for {speciesName} from the selected content packages!");
@@ -828,7 +900,8 @@ namespace Barotrauma
public bool IsKeyHit(InputType inputType)
{
if (GameMain.Server != null && Controlled != this)
#if SERVER
if (GameMain.Server != null && IsRemotePlayer)
{
switch (inputType)
{
@@ -858,13 +931,15 @@ namespace Barotrauma
return false;
}
}
#endif
return keys[(int)inputType].Hit;
}
public bool IsKeyDown(InputType inputType)
{
if (GameMain.Server != null && Character.Controlled != this)
#if SERVER
if (GameMain.Server != null && IsRemotePlayer)
{
switch (inputType)
{
@@ -893,6 +968,7 @@ namespace Barotrauma
}
return false;
}
#endif
return keys[(int)inputType].Held;
}
@@ -901,6 +977,7 @@ namespace Barotrauma
{
keys[(int)inputType].Hit = hit;
keys[(int)inputType].Held = held;
keys[(int)inputType].SetState(hit, held);
}
public void ClearInput(InputType inputType)
@@ -935,8 +1012,10 @@ namespace Barotrauma
{
return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier);
}
float findFocusedTimer;
// TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
public Vector2? OverrideMovement { get; set; }
public bool ForceRun { get; set; }
// TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
public Vector2? OverrideMovement { get; set; }
@@ -985,13 +1064,59 @@ namespace Barotrauma
//apply speed multiplier if
// a. it's boosting the movement speed and the character is trying to move fast (= running)
// b. it's a debuff that decreases movement speed
if (run || SpeedMultiplier <= 0.0f) targetMovement *= SpeedMultiplier;
float speedMultiplier = SpeedMultiplier;
if (run || speedMultiplier <= 0.0f) targetMovement *= speedMultiplier;
SpeedMultiplier = 1; // Reset, items will set the value before the next update
ResetSpeedMultiplier(); // Reset, items will set the value before the next update
return targetMovement;
}
/// <summary>
/// Can be used to modify the character's speed via StatusEffects
/// </summary>
public float SpeedMultiplier
{
get
{
if (speedMultipliers.Count == 0) return 1f;
float greatestPositive = 1f;
float greatestNegative = 1f;
for (int i = 0; i < speedMultipliers.Count; i++)
{
float val = speedMultipliers[i];
if (val < 1f)
{
if (val < greatestNegative)
{
greatestNegative = val;
}
}
else
{
if (val > greatestPositive)
{
greatestPositive = val;
}
}
}
return greatestPositive - (1f - greatestNegative);
}
set
{
if (value == 1f) return;
speedMultipliers.Add(value);
}
}
public void ResetSpeedMultiplier()
{
speedMultipliers.Clear();
}
/// <summary>
/// Applies temporary limits to the speed (damage).
/// </summary>
@@ -1024,12 +1149,23 @@ namespace Barotrauma
ViewTarget = null;
if (!AllowInput) return;
if (!(this is AICharacter) || controlled == this || IsRemotePlayer)
Vector2 smoothedCursorDiff = cursorPosition - SmoothedCursorPosition;
if (Controlled == this)
{
SmoothedCursorPosition = cursorPosition;
}
else
{
smoothedCursorDiff = NetConfig.InterpolateCursorPositionError(smoothedCursorDiff);
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
}
if (!(this is AICharacter) || Controlled == this || IsRemotePlayer)
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < 0.0f;
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
}
if (AnimController is HumanoidAnimController)
@@ -1040,7 +1176,8 @@ namespace Barotrauma
if (AnimController.onGround &&
!AnimController.InWater &&
AnimController.Anim != AnimController.Animation.UsingConstruction &&
AnimController.Anim != AnimController.Animation.CPR)
AnimController.Anim != AnimController.Animation.CPR &&
(GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this))
{
//Limb head = AnimController.GetLimb(LimbType.Head);
// Values lower than this seem to cause constantious flipping when the mouse is near the player and the player is running, because the root collider moves after flipping.
@@ -1058,31 +1195,35 @@ namespace Barotrauma
AnimController.TargetDir = Direction.Right;
}
}
if (GameMain.Server != null && Controlled != this)
if (GameMain.NetworkMember != null)
{
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
if (GameMain.NetworkMember.IsServer)
{
AnimController.TargetDir = Direction.Left;
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
{
AnimController.TargetDir = Direction.Left;
}
else
{
AnimController.TargetDir = Direction.Right;
}
}
else
else if (GameMain.NetworkMember.IsClient && Controlled != this)
{
AnimController.TargetDir = Direction.Right;
}
}
else if (GameMain.Client != null && controlled != this)
{
if (memState.Count > 0)
{
AnimController.TargetDir = memState[0].Direction;
if (memState.Count > 0)
{
AnimController.TargetDir = memState[0].Direction;
}
}
}
// TODO: remove, dev test only
#if DEBUG
if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.F))
{
AnimController.ReleaseStuckLimbs();
}
#endif
if (attackCoolDown > 0.0f)
{
@@ -1482,129 +1623,6 @@ namespace Barotrauma
customInteractHUDText = hudText;
}
private List<Item> debugInteractablesInRange = new List<Item>();
private List<Item> debugInteractablesAtCursor = new List<Item>();
private List<Pair<Item, float>> debugInteractablesNearCursor = new List<Pair<Item, float>>();
/// <summary>
/// Finds the front (lowest depth) interactable item at a position. "Interactable" in this case means that the character can "reach" the item.
/// </summary>
/// <param name="character">The Character who is looking for the interactable item, only items that are close enough to this character are returned</param>
/// <param name="simPosition">The item at the simPosition, with the lowest depth, is returned</param>
/// <param name="allowFindingNearestItem">If this is true and an item cannot be found at simPosition then a nearest item will be returned if possible</param>
/// <param name="hull">If a hull is specified, only items within that hull are returned</param>
public Item FindItemAtPosition(Vector2 simPosition, float aimAssistModifier = 0.0f, Hull hull = null, Item[] ignoredItems = null)
{
if (Submarine != null)
{
simPosition += Submarine.SimPosition;
}
debugInteractablesInRange.Clear();
debugInteractablesAtCursor.Clear();
debugInteractablesNearCursor.Clear();
Vector2 displayPosition = ConvertUnits.ToDisplayUnits(simPosition);
Item closestItem = null;
float closestItemDistance = 0.0f;
foreach (Item item in Item.ItemList)
{
if (ignoredItems != null && ignoredItems.Contains(item)) continue;
if (hull != null && item.CurrentHull != hull) continue;
if (item.body != null && !item.body.Enabled) continue;
if (item.ParentInventory != null) continue;
if (!CanInteractWith(item)) continue;
debugInteractablesInRange.Add(item);
float distanceToItem = float.PositiveInfinity;
if (item.IsInsideTrigger(displayPosition, out Rectangle transformedTrigger))
{
debugInteractablesAtCursor.Add(item);
//distance is between 0-1 when the cursor is directly on the item
distanceToItem =
Math.Abs(transformedTrigger.Center.X - displayPosition.X) / transformedTrigger.Width +
Math.Abs((transformedTrigger.Y - transformedTrigger.Height / 2.0f) - displayPosition.Y) / transformedTrigger.Height;
//modify the distance based on the size of the trigger (preferring smaller items)
distanceToItem *= MathHelper.Lerp(0.05f, 2.0f, (transformedTrigger.Width + transformedTrigger.Height) / 250.0f);
}
else
{
Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height);
if (itemDisplayRect.Contains(displayPosition))
{
debugInteractablesAtCursor.Add(item);
//distance is between 0-1 when the cursor is directly on the item
distanceToItem =
Math.Abs(itemDisplayRect.Center.X - displayPosition.X) / itemDisplayRect.Width +
Math.Abs(itemDisplayRect.Center.Y - displayPosition.Y) / itemDisplayRect.Height;
//modify the distance based on the size of the item (preferring smaller ones)
distanceToItem *= MathHelper.Lerp(0.05f, 2.0f, (itemDisplayRect.Width + itemDisplayRect.Height) / 250.0f);
}
else
{
//get the point on the itemDisplayRect which is closest to the cursor
Vector2 rectIntersectionPoint = new Vector2(
MathHelper.Clamp(displayPosition.X, itemDisplayRect.X, itemDisplayRect.Right),
MathHelper.Clamp(displayPosition.Y, itemDisplayRect.Y, itemDisplayRect.Bottom));
distanceToItem = 2.0f + Vector2.Distance(rectIntersectionPoint, displayPosition);
}
}
//reduce the amount of aim assist if an item has been selected
//= can't switch selection to another item without deselecting the current one first UNLESS the cursor is directly on the item
//otherwise it would be too easy to accidentally switch the selected item when rewiring items
float aimAssistAmount = SelectedConstruction == null ? 100.0f * aimAssistModifier : 1.0f;
if (distanceToItem < Math.Max(aimAssistAmount, 1.0f))
{
debugInteractablesNearCursor.Add(new Pair<Item, float>(item, 1.0f - distanceToItem / (100.0f * aimAssistModifier)));
if (closestItem == null || distanceToItem < closestItemDistance)
{
closestItem = item;
closestItemDistance = distanceToItem;
}
}
}
return closestItem;
}
private Character FindCharacterAtPosition(Vector2 mouseSimPos, float maxDist = 150.0f)
{
Character closestCharacter = null;
float closestDist = 0.0f;
maxDist = ConvertUnits.ToSimUnits(maxDist);
foreach (Character c in CharacterList)
{
if (!CanInteractWith(c)) continue;
float dist = Vector2.DistanceSquared(mouseSimPos, c.SimPosition);
if (dist < maxDist * maxDist && (closestCharacter == null || dist < closestDist))
{
closestCharacter = c;
closestDist = dist;
}
/*FarseerPhysics.Common.Transform transform;
c.AnimController.Collider.FarseerBody.GetTransform(out transform);
for (int i = 0; i < c.AnimController.Collider.FarseerBody.FixtureList.Count; i++)
{
if (c.AnimController.Collider.FarseerBody.FixtureList[i].Shape.TestPoint(ref transform, ref mouseSimPos))
{
Console.WriteLine("Hit: " + i);
closestCharacter = c;
}
}*/
}
return closestCharacter;
}
private void TransformCursorPos()
{
if (Submarine == null)
@@ -1642,7 +1660,8 @@ namespace Barotrauma
public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
{
bool isLocalPlayer = (controlled == this);
bool isLocalPlayer = Controlled == this;
if (!isLocalPlayer && (this is AICharacter && !IsRemotePlayer))
{
return;
@@ -1690,9 +1709,10 @@ namespace Barotrauma
if (SelectedConstruction == null && !AnimController.InWater && Screen.Selected != GameMain.SubEditorScreen)
{
bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down);
bool isControlled = Controlled == this;
Ladder nearbyLadder = null;
if (Controlled == this || climbInput)
if (isControlled || climbInput)
{
float minDist = float.PositiveInfinity;
foreach (Ladder ladder in Ladder.List)
@@ -1701,7 +1721,7 @@ namespace Barotrauma
{
minDist = dist;
nearbyLadder = ladder;
if (Controlled == this) ladder.Item.IsHighlighted = true;
if (isControlled) ladder.Item.IsHighlighted = true;
break;
}
}
@@ -1744,16 +1764,17 @@ namespace Barotrauma
}
else if (focusedItem != null)
{
bool canInteract = focusedItem.TryInteract(this);
#if CLIENT
if (Controlled == this)
{
focusedItem.IsHighlighted = true;
if (canInteract)
{
CharacterHealth.OpenHealthWindow = null;
}
}
if (focusedItem.TryInteract(this))
{
#if CLIENT
if (Controlled == this) CharacterHealth.OpenHealthWindow = null;
#endif
}
}
else if (IsKeyHit(InputType.Select) && SelectedConstruction != null)
{
@@ -1776,16 +1797,16 @@ namespace Barotrauma
public static void UpdateAll(float deltaTime, Camera cam)
{
if (GameMain.Client == null)
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
foreach (Character c in CharacterList)
{
if (!(c is AICharacter) && !c.IsRemotePlayer) continue;
if (GameMain.Server != null)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
//disable AI characters that are far away from all clients and the host's character and not controlled by anyone
if (c == controlled || c.IsRemotePlayer)
if (c == Controlled || c.IsRemotePlayer)
{
c.Enabled = true;
}
@@ -1794,8 +1815,7 @@ namespace Barotrauma
float distSqr = float.MaxValue;
foreach (Character otherCharacter in CharacterList)
{
if (otherCharacter == c) { continue; }
if (!otherCharacter.IsRemotePlayer && otherCharacter != GameMain.NetworkMember.Character) { continue; }
if (otherCharacter == c || !otherCharacter.IsRemotePlayer) { continue; }
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.WorldPosition, c.WorldPosition));
}
@@ -1815,7 +1835,7 @@ namespace Barotrauma
float distSqr = Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition);
if (Controlled != null)
{
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(controlled.WorldPosition, c.WorldPosition));
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, c.WorldPosition));
}
if (distSqr > NetConfig.DisableCharacterDistSqr)
@@ -1827,6 +1847,7 @@ namespace Barotrauma
c.Enabled = true;
}
}
}
}
@@ -1839,8 +1860,8 @@ namespace Barotrauma
public virtual void Update(float deltaTime, Camera cam)
{
UpdateProjSpecific(deltaTime, cam);
if (GameMain.Client != null && this == Controlled && !isSynced) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) return;
if (!Enabled) return;
@@ -1910,8 +1931,8 @@ namespace Barotrauma
if (PressureTimer >= 100.0f)
{
if (controlled == this) cam.Zoom = 5.0f;
if (GameMain.Client == null)
if (Controlled == this) cam.Zoom = 5.0f;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Implode();
return;
@@ -1923,7 +1944,7 @@ namespace Barotrauma
PressureTimer = 0.0f;
}
}
else if (GameMain.Client == null && WorldPosition.Y < CharacterHealth.CrushDepth)
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && WorldPosition.Y < CharacterHealth.CrushDepth)
{
//implode if below crush depth, and either outside or in a high-pressure hull
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
@@ -1950,9 +1971,10 @@ namespace Barotrauma
UpdateAIChatMessages(deltaTime);
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
bool allowRagdoll = GameMain.NetworkMember != null ? GameMain.NetworkMember.ServerSettings.AllowRagdollButton : true;
if (IsForceRagdolled)
IsRagdolled = IsForceRagdolled;
else if ((GameMain.Server == null || GameMain.Server.AllowRagdollButton) && (!IsRagdolled || AnimController.Collider.LinearVelocity.Length() < 1f)) //Keep us ragdolled if we were forced or we're too speedy to unragdoll
else if (allowRagdoll && (!IsRagdolled || AnimController.Collider.LinearVelocity.LengthSquared() < 1f)) //Keep us ragdolled if we were forced or we're too speedy to unragdoll
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
UpdateSightRange();
@@ -1974,7 +1996,10 @@ namespace Barotrauma
//AI and control stuff
Control(deltaTime, cam);
if (controlled != this && (!(this is AICharacter) || IsRemotePlayer))
bool isNotControlled = Controlled != this;
if (isNotControlled && (!(this is AICharacter) || IsRemotePlayer))
{
Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
DoInteractionUpdate(deltaTime, mouseSimPos);
@@ -2065,7 +2090,7 @@ namespace Barotrauma
public void Speak(string message, ChatMessageType? messageType, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f)
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
//already sent a similar message a moment ago
if (!string.IsNullOrEmpty(identifier) && minDurationBetweenSimilar > 0.0f &&
@@ -2080,7 +2105,7 @@ namespace Barotrauma
private void UpdateAIChatMessages(float deltaTime)
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
List<AIChatMessage> sentMessages = new List<AIChatMessage>();
foreach (AIChatMessage message in aiChatMessageQueue)
@@ -2102,10 +2127,12 @@ namespace Barotrauma
}
}
#endif
#if SERVER
if (GameMain.Server != null && message.MessageType != ChatMessageType.Order)
{
GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this);
}
#endif
ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.MessageType.Value]);
sentMessages.Add(message);
}
@@ -2134,23 +2161,7 @@ namespace Barotrauma
speechBubbleColor = color;
}
private void AdjustKarma(Character attacker, AttackResult attackResult)
{
if (GameMain.Server == null || attacker == null) return;
Client attackerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == attacker);
if (attackerClient == null) return;
Client targetClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
if (targetClient != null || this == Controlled)
{
if (attacker.TeamID == TeamID)
{
attackerClient.Karma -= attackResult.Damage * 0.01f;
if (CharacterHealth.MaxVitality <= CharacterHealth.MinVitality) attackerClient.Karma = 0.0f;
}
}
}
partial void AdjustKarma(Character attacker, AttackResult attackResult);
partial void DamageHUD(float amount);
@@ -2186,9 +2197,9 @@ namespace Barotrauma
DamageLimb(worldPosition, targetLimb, attack.Afflictions, attack.Stun, playSound, attackImpulse, attacker);
if (limbHit == null) return new AttackResult();
limbHit.body?.ApplyLinearImpulse(attack.TargetImpulseWorld + attack.TargetForceWorld * deltaTime);
#if SERVER
if (attacker is Character attackingCharacter && attackingCharacter.AIController == null)
{
string logMsg = LogName + " attacked by " + attackingCharacter.LogName + ".";
@@ -2202,8 +2213,11 @@ namespace Barotrauma
}
GameServer.Log(logMsg, ServerLog.MessageType.Attack);
}
#endif
if (GameMain.Client == null &&
bool isNotClient = GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient;
if (isNotClient &&
IsDead && Rand.Range(0.0f, 1.0f) < attack.SeverLimbsProbability)
{
foreach (LimbJoint joint in AnimController.LimbJoints)
@@ -2264,17 +2278,21 @@ namespace Barotrauma
if (Removed) return new AttackResult();
SetStun(stun);
Vector2 dir = hitLimb.WorldPosition - worldPosition;
if (Math.Abs(attackImpulse) > 0.0f)
{
Vector2 diff = hitLimb.WorldPosition - worldPosition;
Vector2 diff = dir;
if (diff == Vector2.Zero) diff = Rand.Vector(1.0f);
hitLimb.body.ApplyLinearImpulse(Vector2.Normalize(diff) * attackImpulse, hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff));
}
AttackResult attackResult = hitLimb.AddDamage(worldPosition, afflictions, playSound);
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound);
CharacterHealth.ApplyDamage(hitLimb, attackResult);
if (attacker != this) { OnAttacked?.Invoke(attacker, attackResult); };
if (attacker != this)
{
OnAttacked?.Invoke(attacker, attackResult);
OnAttackedProjSpecific(attacker, attackResult);
};
AdjustKarma(attacker, attackResult);
if (attacker != null && attackResult.Damage > 0.0f)
@@ -2285,9 +2303,11 @@ namespace Barotrauma
return attackResult;
}
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult);
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
{
if (GameMain.Client != null && !isNetworkMessage) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) return;
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) return;
@@ -2304,8 +2324,18 @@ namespace Barotrauma
{
foreach (StatusEffect statusEffect in statusEffects)
{
if (statusEffect.type != actionType) continue;
statusEffect.Apply(actionType, deltaTime, this, this);
if (statusEffect.type != actionType) { continue; }
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
statusEffect.GetNearbyTargets(WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, this, targets);
}
else
{
statusEffect.Apply(actionType, deltaTime, this, this);
}
}
}
@@ -2315,7 +2345,7 @@ namespace Barotrauma
if (!isNetworkMessage)
{
if (GameMain.Client != null) return;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
}
Kill(CauseOfDeathType.Pressure, null, isNetworkMessage);
@@ -2359,8 +2389,10 @@ namespace Barotrauma
{
if (IsDead || CharacterHealth.Unkillable) { return; }
HealthUpdateInterval = 0.0f;
//clients aren't allowed to kill characters unless they receive a network message
if (!isNetworkMessage && GameMain.Client != null)
if (!isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
@@ -2369,19 +2401,11 @@ namespace Barotrauma
AnimController.Frozen = false;
if (causeOfDeath == CauseOfDeathType.Affliction)
{
GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeathAffliction.Prefab.Name + ")", ServerLog.MessageType.Attack);
}
else
{
GameServer.Log(LogName + " has died (Cause of death: " + causeOfDeath + ")", ServerLog.MessageType.Attack);
}
if (GameSettings.SendUserStatistics)
{
string characterType = "Unknown";
if (this == controlled)
if (this == Controlled)
characterType = "Player";
else if (IsRemotePlayer)
characterType = "RemotePlayer";
@@ -2402,7 +2426,7 @@ namespace Barotrauma
SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath);
KillProjSpecific();
KillProjSpecific(causeOfDeath, causeOfDeathAffliction);
IsDead = true;
@@ -2427,7 +2451,7 @@ namespace Barotrauma
GameMain.GameSession.KillCharacter(this);
}
}
partial void KillProjSpecific();
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction);
public void Revive()
{
@@ -1,7 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -49,12 +48,12 @@ namespace Barotrauma
public XElement BeardElement { get; set; }
public XElement MoustacheElement { get; set; }
public XElement FaceAttachment { get; set; }
public HeadInfo() { }
public HeadInfo(int headId)
{
_headSpriteId = headId;
_headSpriteId = Math.Max(headId, 1);
}
public void ResetAttachmentIndices()
@@ -66,7 +65,7 @@ namespace Barotrauma
}
}
private HeadInfo head = new HeadInfo();
private HeadInfo head;
public HeadInfo Head
{
get { return head; }
@@ -79,10 +78,6 @@ namespace Barotrauma
{
head.race = GetRandomRace();
}
if (head.gender == Gender.None)
{
head.gender = GetRandomGender();
}
CalculateHeadSpriteRange();
Head.HeadSpriteId = value.HeadSpriteId;
HeadSprite = null;
@@ -101,16 +96,15 @@ namespace Barotrauma
get
{
string disguiseName = "?";
if (Character == null || !Character.HideFace || (GameMain.Server != null && !GameMain.Server.AllowDisguises))
if (Character == null || !Character.HideFace)
{
return Name;
}
#if CLIENT
if (GameMain.Client != null && !GameMain.Client.AllowDisguises)
else if ((GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises))
{
return Name;
}
#endif
if (Character.Inventory != null)
{
int cardSlotIndex = Character.Inventory.FindLimbSlot(InvSlotType.Card);
@@ -134,6 +128,8 @@ namespace Barotrauma
}
}
public string SpeciesName => SourceElement.GetAttributeString("name", string.Empty);
/// <summary>
/// Note: Can be null.
/// </summary>
@@ -237,78 +233,11 @@ namespace Barotrauma
public readonly string ragdollFileName = string.Empty;
private Sprite headSprite;
public Sprite HeadSprite
{
get
{
if (headSprite == null)
{
LoadHeadSprite();
}
return headSprite;
}
}
private Sprite portrait;
public Sprite Portrait
{
get
{
if (portrait == null)
{
LoadHeadSprite();
}
return portrait;
}
}
private Sprite clothingSprite;
public Sprite ClothingSprite
{
get
{
if (clothingSprite == null)
{
if (Job != null && Job.Prefab.ClothingElement != null)
{
clothingSprite = new Sprite(Job.Prefab.ClothingElement.Element("sprite"));
}
}
return clothingSprite;
}
}
private NPCPersonalityTrait personalityTrait;
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
public XElement InventoryData;
public XElement SourceElement { get; set; }
public XElement HairElement { get; private set; }
public XElement BeardElement { get; private set; }
public XElement MoustacheElement { get; private set; }
public XElement FaceAttachment { get; private set; }
public int HairIndex { get; set; } = -1;
public int BeardIndex { get; set; } = -1;
public int MoustacheIndex { get; set; } = -1;
public int FaceAttachmentIndex { get; set; } = -1;
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
public readonly string ragdollFileName = string.Empty;
public bool StartItemsGiven;
public CauseOfDeath CauseOfDeath;
public byte TeamID;
public Character.TeamType TeamID;
private NPCPersonalityTrait personalityTrait;
@@ -317,7 +246,6 @@ namespace Barotrauma
public ushort ID;
public XElement InventoryData;
public List<string> SpriteTags
{
@@ -345,7 +273,8 @@ namespace Barotrauma
}
}
private Gender gender;
public readonly bool HasGenders;
public Gender Gender
{
get { return Head.gender; }
@@ -399,7 +328,7 @@ namespace Barotrauma
{
if (ragdoll == null)
{
string speciesName = SourceElement.GetAttributeString("name", string.Empty);
string speciesName = SpeciesName;
bool isHumanoid = SourceElement.GetAttributeBool("humanoid", false);
ragdoll = isHumanoid
? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName)
@@ -413,7 +342,7 @@ namespace Barotrauma
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// Used for creating the data
public CharacterInfo(string file, string name = "", Gender gender = Gender.None, JobPrefab jobPrefab = null, string ragdollFileName = null)
public CharacterInfo(string file, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null)
{
ID = idCounter;
idCounter++;
@@ -421,18 +350,13 @@ namespace Barotrauma
SpriteTags = new List<string>();
XDocument doc = GetConfig(file);
SourceElement = doc.Root;
if (doc.Root.GetAttributeBool("genders", false))
head = new HeadInfo();
HasGenders = doc.Root.GetAttributeBool("genders", false);
if (HasGenders)
{
Head.gender = gender == Gender.None ? GetRandomGender() : gender;
}
if (!Enum.TryParse(doc.Root.GetAttributeString("race", "None"), true, out Head.race))
{
Head.race = GetRandomRace();
}
if (Head.race == Race.None)
{
Head.race = GetRandomRace();
Head.gender = GetRandomGender();
}
Head.race = GetRandomRace();
CalculateHeadSpriteRange();
Head.HeadSpriteId = GetRandomHeadID();
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab);
@@ -477,16 +401,28 @@ namespace Barotrauma
idCounter++;
Name = element.GetAttributeString("name", "unnamed");
string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant();
Head.gender = (genderStr == "male") ? Gender.Male : Gender.Female;
Enum.TryParse(element.GetAttributeString("race", "white"), true, out Head.race);
File = element.GetAttributeString("file", "");
SourceElement = GetConfig(File).Root;
HasGenders = SourceElement.GetAttributeBool("genders", false);
Salary = element.GetAttributeInt("salary", 1000);
Head.HeadSpriteId = element.GetAttributeInt("headspriteid", 1);
Head.HairIndex = element.GetAttributeInt("hairindex", -1);
Head.BeardIndex = element.GetAttributeInt("beardindex", -1);
Head.MoustacheIndex = element.GetAttributeInt("moustacheindex", -1);
Head.FaceAttachmentIndex = element.GetAttributeInt("faceattachmentindex", -1);
Enum.TryParse(element.GetAttributeString("race", "White"), true, out Race race);
Enum.TryParse(element.GetAttributeString("gender", "None"), true, out Gender gender);
if (HasGenders && gender == Gender.None)
{
gender = GetRandomGender();
}
else if (!HasGenders)
{
gender = Gender.None;
}
RecreateHead(
element.GetAttributeInt("headspriteid", 1),
race,
gender,
element.GetAttributeInt("hairindex", -1),
element.GetAttributeInt("beardindex", -1),
element.GetAttributeInt("moustacheindex", -1),
element.GetAttributeInt("faceattachmentindex", -1));
StartItemsGiven = element.GetAttributeBool("startitemsgiven", false);
string personalityName = element.GetAttributeString("personality", "");
ragdollFileName = element.GetAttributeString("ragdoll", string.Empty);
@@ -503,50 +439,6 @@ namespace Barotrauma
LoadHeadAttachments();
}
public void LoadHeadSprite()
{
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") continue;
XElement spriteElement = limbElement.Element("sprite");
string spritePath = spriteElement.Attribute("texture").Value;
spritePath = spritePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
spritePath = spritePath.Replace("[RACE]", Head.race.ToString().ToLowerInvariant());
spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString());
string fileName = Path.GetFileNameWithoutExtension(spritePath);
//go through the files in the directory to find a matching sprite
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
{
if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
fileWithoutTags = fileWithoutTags.Split('[', ']').First();
if (fileWithoutTags != fileName) continue;
HeadSprite = new Sprite(spriteElement, "", file);
Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
//extract the tags out of the filename
SpriteTags = file.Split('[', ']').Skip(1).ToList();
if (SpriteTags.Any())
{
SpriteTags.RemoveAt(SpriteTags.Count-1);
}
break;
}
break;
}
}
private XDocument GetConfig(string file)
{
if (!cachedConfigs.TryGetValue(file, out XDocument doc))
@@ -558,8 +450,6 @@ namespace Barotrauma
return doc;
}
public Gender SetRandomGender() => Gender = GetRandomGender();
public Race SetRandomRace() => Race = GetRandomRace();
public int SetRandomHead() => HeadSpriteId = GetRandomHeadID();
public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < SourceElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
@@ -592,7 +482,7 @@ namespace Barotrauma
{
if (elements == null) { return elements; }
return elements.Where(w =>
Enum.TryParse(w.GetAttributeString("gender", "male"), true, out Gender g) && g == Head.gender &&
Enum.TryParse(w.GetAttributeString("gender", "None"), true, out Gender g) && g == Head.gender &&
Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == Head.race);
}
@@ -600,11 +490,13 @@ namespace Barotrauma
{
if (SourceElement == null) { return; }
Head.headSpriteRange = SourceElement.GetAttributeVector2("headidrange", Vector2.Zero);
// If range is defined, we use it as it is
// Else we calculate the range from the wearables.
if (Head.headSpriteRange == Vector2.Zero)
{
// If range is defined, we use it as it is
// Else we calculate the range from the wearables.
var wearables = FilterElementsByGenderAndRace(Wearables);
var wearableElements = Wearables;
if (wearableElements == null) { return; }
var wearables = FilterElementsByGenderAndRace(wearableElements).ToList();
if (wearables == null)
{
Head.headSpriteRange = Vector2.Zero;
@@ -630,6 +522,74 @@ namespace Barotrauma
}
}
public void RecreateHead(int headID, Race race, Gender gender, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
{
if (HasGenders && gender == Gender.None)
{
gender = GetRandomGender();
}
else if (!HasGenders)
{
gender = Gender.None;
}
head = new HeadInfo(headID)
{
race = race,
gender = gender,
HairIndex = hairIndex,
BeardIndex = beardIndex,
MoustacheIndex = moustacheIndex,
FaceAttachmentIndex = faceAttachmentIndex
};
CalculateHeadSpriteRange();
ReloadHeadAttachments();
}
public void LoadHeadSprite()
{
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
{
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") continue;
XElement spriteElement = limbElement.Element("sprite");
string spritePath = spriteElement.Attribute("texture").Value;
spritePath = spritePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male");
spritePath = spritePath.Replace("[RACE]", Head.race.ToString().ToLowerInvariant());
spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString());
string fileName = Path.GetFileNameWithoutExtension(spritePath);
//go through the files in the directory to find a matching sprite
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
{
if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
fileWithoutTags = fileWithoutTags.Split('[', ']').First();
if (fileWithoutTags != fileName) continue;
HeadSprite = new Sprite(spriteElement, "", file);
Portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
//extract the tags out of the filename
SpriteTags = file.Split('[', ']').Skip(1).ToList();
if (SpriteTags.Any())
{
SpriteTags.RemoveAt(SpriteTags.Count - 1);
}
break;
}
break;
}
}
/// <summary>
/// Loads only the elements according to the indices, not the sprites.
/// </summary>
@@ -762,7 +722,7 @@ namespace Barotrauma
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos)
{
if (Job == null || GameMain.Client != null) return;
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)) return;
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase);
@@ -771,9 +731,9 @@ namespace Barotrauma
OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos);
if (GameMain.Server != null && (int)newLevel != (int)prevLevel)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (int)newLevel != (int)prevLevel)
{
GameMain.Server.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills });
GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills });
}
}
@@ -845,7 +805,7 @@ namespace Barotrauma
int slotIndex = itemElement.GetAttributeInt("i", 0);
if (newItem == null) continue;
Entity.Spawner.CreateNetworkEvent(newItem, false);
SpawnInventoryItemProjSpecific(newItem);
inventory.TryPutItem(newItem, slotIndex, false, false, null);
@@ -860,96 +820,8 @@ namespace Barotrauma
}
}
}
public void ServerWrite(NetBuffer msg)
{
msg.Write(ID);
msg.Write(Name);
msg.Write(Gender == Gender.Female);
msg.Write((byte)Race);
msg.Write((byte)HeadSpriteId);
msg.Write((byte)Head.HairIndex);
msg.Write((byte)Head.BeardIndex);
msg.Write((byte)Head.MoustacheIndex);
msg.Write((byte)Head.FaceAttachmentIndex);
msg.Write(ragdollFileName);
if (Job != null)
{
msg.Write(Job.Prefab.Identifier);
msg.Write((byte)Job.Skills.Count);
foreach (Skill skill in Job.Skills)
{
msg.Write(skill.Identifier);
msg.Write(skill.Level);
}
}
else
{
msg.Write("");
}
// TODO: animations
}
public static CharacterInfo ClientRead(string configPath, NetBuffer inc)
{
ushort infoID = inc.ReadUInt16();
string newName = inc.ReadString();
bool isFemale = inc.ReadBoolean();
int race = inc.ReadByte();
int headSpriteID = inc.ReadByte();
int hairIndex = inc.ReadByte();
int beardIndex = inc.ReadByte();
int moustacheIndex = inc.ReadByte();
int faceAttachmentIndex = inc.ReadByte();
string ragdollFile = inc.ReadString();
string jobIdentifier = inc.ReadString();
JobPrefab jobPrefab = null;
Dictionary<string, float> skillLevels = new Dictionary<string, float>();
if (!string.IsNullOrEmpty(jobIdentifier))
{
jobPrefab = JobPrefab.List.Find(jp => jp.Identifier == jobIdentifier);
int skillCount = inc.ReadByte();
for (int i = 0; i < skillCount; i++)
{
string skillIdentifier = inc.ReadString();
float skillLevel = inc.ReadSingle();
skillLevels.Add(skillIdentifier, skillLevel);
}
}
// TODO: animations
CharacterInfo ch = new CharacterInfo(configPath, newName, isFemale ? Gender.Female : Gender.Male, jobPrefab, ragdollFile)
{
ID = infoID,
};
ch.Head.race = (Race)race;
ch.Head.HeadSpriteId = headSpriteID;
ch.HairIndex = hairIndex;
ch.BeardIndex = beardIndex;
ch.MoustacheIndex = moustacheIndex;
ch.FaceAttachmentIndex = faceAttachmentIndex;
ch.CalculateHeadSpriteRange();
ch.ReloadHeadAttachments();
System.Diagnostics.Debug.Assert(skillLevels.Count == ch.Job.Skills.Count);
if (ch.Job != null)
{
foreach (KeyValuePair<string, float> skill in skillLevels)
{
Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key);
if (matchingSkill == null)
{
DebugConsole.ThrowError("Skill \"" + skill.Key + "\" not found in character \"" + newName + "\"");
continue;
}
matchingSkill.Level = skill.Value;
}
}
return ch;
}
partial void SpawnInventoryItemProjSpecific(Item item);
public void ReloadHeadAttachments()
{
@@ -1,9 +1,7 @@
using Barotrauma.Networking;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -15,18 +13,18 @@ namespace Barotrauma
public readonly AnimController.Animation Animation;
public CharacterStateInfo(Vector2 pos, float rotation, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: this(pos, rotation, 0, time, dir, interact, animation)
public CharacterStateInfo(Vector2 pos, float rotation, Vector2 velocity, float angularVelocity, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: this(pos, rotation, velocity, angularVelocity, 0, time, dir, interact, animation)
{
}
public CharacterStateInfo(Vector2 pos, float rotation, UInt16 ID, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: this(pos, rotation, ID, 0.0f, dir, interact, animation)
: this(pos, rotation, Vector2.Zero, 0.0f, ID, 0.0f, dir, interact, animation)
{
}
protected CharacterStateInfo(Vector2 pos, float rotation, UInt16 ID, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: base(pos, rotation, ID, time)
protected CharacterStateInfo(Vector2 pos, float rotation, Vector2 velocity, float angularVelocity, UInt16 ID, float time, Direction dir, Entity interact, AnimController.Animation animation = AnimController.Animation.None)
: base(pos, rotation, velocity, angularVelocity, ID, time)
{
Direction = dir;
Interact = interact;
@@ -81,16 +79,22 @@ namespace Barotrauma
private List<CharacterStateInfo> memState = new List<CharacterStateInfo>();
private List<CharacterStateInfo> memLocalState = new List<CharacterStateInfo>();
public float healthUpdateTimer;
private float healthUpdateInterval;
public float HealthUpdateInterval
{
get { return healthUpdateInterval; }
set
{
healthUpdateInterval = MathHelper.Clamp(value, 0.0f, IsDead ? NetConfig.MaxHealthUpdateIntervalDead : NetConfig.MaxHealthUpdateInterval);
healthUpdateTimer = Math.Min(healthUpdateTimer, healthUpdateInterval);
}
}
private bool networkUpdateSent;
public bool isSynced = false;
public string OwnerClientIP;
public string OwnerClientName;
public bool ClientDisconnected;
public float KillDisconnectedTimer;
public List<CharacterStateInfo> MemState
{
get { return memState; }
@@ -110,512 +114,7 @@ namespace Barotrauma
LastNetworkUpdateID = 0;
LastProcessedID = 0;
}
private void UpdateNetInput()
{
if (this != Controlled)
{
if (GameMain.Client != null)
{
#if CLIENT
//freeze AI characters if more than 1 seconds have passed since last update from the server
if (lastRecvPositionUpdateTime < NetTime.Now - 1.0f)
{
AnimController.Frozen = true;
memState.Clear();
//hide after 2 seconds
if (lastRecvPositionUpdateTime < NetTime.Now - 2.0f)
{
Enabled = false;
return;
}
}
#endif
}
else if (GameMain.Server != null && (!(this is AICharacter) || IsRemotePlayer))
{
if (!AllowInput)
{
AnimController.Frozen = false;
if (memInput.Count > 0)
{
prevDequeuedInput = dequeuedInput;
dequeuedInput = memInput[memInput.Count - 1].states;
memInput.RemoveAt(memInput.Count - 1);
}
}
else if (memInput.Count == 0)
{
AnimController.Frozen = true;
}
else
{
AnimController.Frozen = false;
prevDequeuedInput = dequeuedInput;
LastProcessedID = memInput[memInput.Count - 1].networkUpdateID;
dequeuedInput = memInput[memInput.Count - 1].states;
double aimAngle = ((double)memInput[memInput.Count - 1].intAim / 65535.0) * 2.0 * Math.PI;
cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 60.0f;
//reset focus when attempting to use/select something
if (memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Use) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Select) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Health) ||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Grab))
{
focusedItem = null;
focusedCharacter = null;
}
var closestEntity = FindEntityByID(memInput[memInput.Count - 1].interact);
if (closestEntity is Item)
{
if (CanInteractWith((Item)closestEntity))
{
focusedItem = (Item)closestEntity;
focusedCharacter = null;
}
}
else if (closestEntity is Character)
{
if (CanInteractWith((Character)closestEntity))
{
focusedCharacter = (Character)closestEntity;
focusedItem = null;
}
}
memInput.RemoveAt(memInput.Count - 1);
TransformCursorPos();
if ((dequeuedInput == InputNetFlags.None || dequeuedInput == InputNetFlags.FacingLeft) && Math.Abs(AnimController.Collider.LinearVelocity.X) < 0.005f && Math.Abs(AnimController.Collider.LinearVelocity.Y) < 0.2f)
{
while (memInput.Count > 5 && memInput[memInput.Count - 1].states == dequeuedInput)
{
//remove inputs where the player is not moving at all
//helps the server catch up, shouldn't affect final position
LastProcessedID = memInput[memInput.Count - 1].networkUpdateID;
memInput.RemoveAt(memInput.Count - 1);
}
}
}
}
}
else if (GameMain.Client != null)
{
var posInfo = new CharacterStateInfo(
SimPosition,
AnimController.Collider.Rotation,
LastNetworkUpdateID,
AnimController.TargetDir,
SelectedCharacter == null ? (Entity)SelectedConstruction : (Entity)SelectedCharacter,
AnimController.Anim);
memLocalState.Add(posInfo);
InputNetFlags newInput = InputNetFlags.None;
if (IsKeyDown(InputType.Left)) newInput |= InputNetFlags.Left;
if (IsKeyDown(InputType.Right)) newInput |= InputNetFlags.Right;
if (IsKeyDown(InputType.Up)) newInput |= InputNetFlags.Up;
if (IsKeyDown(InputType.Down)) newInput |= InputNetFlags.Down;
if (IsKeyDown(InputType.Run)) newInput |= InputNetFlags.Run;
if (IsKeyDown(InputType.Crouch)) newInput |= InputNetFlags.Crouch;
if (IsKeyHit(InputType.Select)) newInput |= InputNetFlags.Select; //TODO: clean up the way this input is registered
if (IsKeyHit(InputType.Health)) newInput |= InputNetFlags.Health;
if (IsKeyHit(InputType.Grab)) newInput |= InputNetFlags.Grab;
if (IsKeyDown(InputType.Use)) newInput |= InputNetFlags.Use;
if (IsKeyDown(InputType.Aim)) newInput |= InputNetFlags.Aim;
if (IsKeyDown(InputType.Attack)) newInput |= InputNetFlags.Attack;
if (IsKeyDown(InputType.Ragdoll)) newInput |= InputNetFlags.Ragdoll;
if (AnimController.TargetDir == Direction.Left) newInput |= InputNetFlags.FacingLeft;
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
relativeCursorPos.Normalize();
UInt16 intAngle = (UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI));
NetInputMem newMem = new NetInputMem
{
states = newInput,
intAim = intAngle
};
if (focusedItem != null && (!newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health)))
{
newMem.interact = focusedItem.ID;
}
else if (focusedCharacter != null)
{
newMem.interact = focusedCharacter.ID;
}
memInput.Insert(0, newMem);
LastNetworkUpdateID++;
if (memInput.Count > 60)
{
memInput.RemoveRange(60, memInput.Count - 60);
}
}
else //this == Character.Controlled && GameMain.Client == null
{
AnimController.Frozen = false;
}
if (networkUpdateSent)
{
foreach (Key key in keys)
{
key.DequeueHit();
key.DequeueHeld();
}
networkUpdateSent = false;
}
}
public virtual void ServerRead(ClientNetObject type, NetBuffer msg, Client c)
{
if (GameMain.Server == null) return;
switch (type)
{
case ClientNetObject.CHARACTER_INPUT:
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
UInt16 networkUpdateID = msg.ReadUInt16();
byte inputCount = msg.ReadByte();
if (AllowInput) Enabled = true;
for (int i = 0; i < inputCount; i++)
{
InputNetFlags newInput = (InputNetFlags)msg.ReadRangedInteger(0, (int)InputNetFlags.MaxVal);
UInt16 newAim = 0;
UInt16 newInteract = 0;
if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft)
{
c.KickAFKTimer = 0.0f;
}
else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft))
{
//character changed the direction they're facing
c.KickAFKTimer = 0.0f;
}
if (newInput.HasFlag(InputNetFlags.Aim))
{
newAim = msg.ReadUInt16();
}
if (newInput.HasFlag(InputNetFlags.Select) ||
newInput.HasFlag(InputNetFlags.Use) ||
newInput.HasFlag(InputNetFlags.Health) ||
newInput.HasFlag(InputNetFlags.Grab))
{
newInteract = msg.ReadUInt16();
}
//if (AllowInput)
//{
if (NetIdUtils.IdMoreRecent((ushort)(networkUpdateID - i), LastNetworkUpdateID) && (i < 60))
{
NetInputMem newMem = new NetInputMem();
newMem.states = newInput;
newMem.intAim = newAim;
newMem.interact = newInteract;
newMem.networkUpdateID = (ushort)(networkUpdateID - i);
memInput.Insert(i, newMem);
}
//}
}
if (NetIdUtils.IdMoreRecent(networkUpdateID, LastNetworkUpdateID))
{
LastNetworkUpdateID = networkUpdateID;
}
if (memInput.Count > 60)
{
//deleting inputs from the queue here means the server is way behind and data needs to be dropped
//we'll make the server drop down to 30 inputs for good measure
memInput.RemoveRange(30, memInput.Count - 30);
}
break;
case ClientNetObject.ENTITY_STATE:
int eventType = msg.ReadRangedInteger(0,3);
switch (eventType)
{
case 0:
Inventory.ServerRead(type, msg, c);
break;
case 1:
bool doingCPR = msg.ReadBoolean();
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
AnimController.Anim = doingCPR ? AnimController.Animation.CPR : AnimController.Animation.None;
break;
case 2:
if (c.Character != this)
{
#if DEBUG
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
#endif
return;
}
if (IsUnconscious)
{
var causeOfDeath = CharacterHealth.GetCauseOfDeath();
Kill(causeOfDeath.First, causeOfDeath.Second);
}
break;
}
break;
}
msg.ReadPadBits();
}
public virtual void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
{
if (GameMain.Server == null) return;
if (extraData != null)
{
switch ((NetEntityEvent.Type)extraData[0])
{
case NetEntityEvent.Type.InventoryState:
msg.WriteRangedInteger(0, 3, 0);
Inventory.ClientWrite(msg, extraData);
break;
case NetEntityEvent.Type.Control:
msg.WriteRangedInteger(0, 3, 1);
Client owner = ((Client)extraData[1]);
msg.Write(owner == null ? (byte)0 : owner.ID);
break;
case NetEntityEvent.Type.Status:
msg.WriteRangedInteger(0, 3, 2);
WriteStatus(msg);
break;
case NetEntityEvent.Type.UpdateSkills:
msg.WriteRangedInteger(0, 3, 3);
if (Info?.Job == null)
{
msg.Write((byte)0);
}
else
{
msg.Write((byte)Info.Job.Skills.Count);
foreach (Skill skill in Info.Job.Skills)
{
msg.Write(skill.Identifier);
msg.Write(skill.Level);
}
}
break;
default:
DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")");
break;
}
msg.WritePadBits();
}
else
{
msg.Write(ID);
NetBuffer tempBuffer = new NetBuffer();
if (this == c.Character)
{
tempBuffer.Write(true);
if (LastNetworkUpdateID < memInput.Count + 1)
{
tempBuffer.Write((UInt16)0);
}
else
{
tempBuffer.Write((UInt16)(LastNetworkUpdateID - memInput.Count - 1));
}
}
else
{
tempBuffer.Write(false);
bool aiming = false;
bool use = false;
bool attack = false;
if (IsRemotePlayer)
{
aiming = dequeuedInput.HasFlag(InputNetFlags.Aim);
use = dequeuedInput.HasFlag(InputNetFlags.Use);
attack = dequeuedInput.HasFlag(InputNetFlags.Attack);
}
else if (keys != null)
{
aiming = keys[(int)InputType.Aim].GetHeldQueue;
use = keys[(int)InputType.Use].GetHeldQueue;
attack = keys[(int)InputType.Attack].GetHeldQueue;
networkUpdateSent = true;
}
tempBuffer.Write(aiming);
tempBuffer.Write(use);
if (AnimController is HumanoidAnimController)
{
tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching);
}
AttackContext currentContext = GetAttackContext();
// TODO: do we need to filter the attack target here?
bool hasAttackLimb = AnimController.Limbs.Any(l => l != null && l.attack != null && l.attack.IsValidContext(currentContext));
tempBuffer.Write(hasAttackLimb);
if (hasAttackLimb) tempBuffer.Write(attack);
if (aiming)
{
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI)));
}
tempBuffer.Write(IsRagdolled);
tempBuffer.Write(AnimController.TargetDir == Direction.Right);
}
if (SelectedCharacter != null || SelectedConstruction != null)
{
tempBuffer.Write(true);
tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : SelectedConstruction.ID);
if (SelectedCharacter != null)
{
tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR);
}
}
else
{
tempBuffer.Write(false);
}
tempBuffer.Write(SimPosition.X);
tempBuffer.Write(SimPosition.Y);
tempBuffer.Write(AnimController.Collider.Rotation);
WriteStatus(tempBuffer);
tempBuffer.WritePadBits();
msg.Write((byte)tempBuffer.LengthBytes);
msg.Write(tempBuffer);
}
}
private void WriteStatus(NetBuffer msg)
{
if (GameMain.Client != null)
{
DebugConsole.ThrowError("Client attempted to write character status to a networked message");
return;
}
msg.Write(IsDead);
if (IsDead)
{
msg.WriteRangedInteger(0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1, (int)CauseOfDeath.Type);
if (CauseOfDeath.Type == CauseOfDeathType.Affliction)
{
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(CauseOfDeath.Affliction));
}
if (AnimController?.LimbJoints == null)
{
//0 limbs severed
msg.Write((byte)0);
}
else
{
List<int> severedJointIndices = new List<int>();
for (int i = 0; i < AnimController.LimbJoints.Length; i++)
{
if (AnimController.LimbJoints[i] != null && AnimController.LimbJoints[i].IsSevered)
{
severedJointIndices.Add(i);
}
}
msg.Write((byte)severedJointIndices.Count);
foreach (int jointIndex in severedJointIndices)
{
msg.Write((byte)jointIndex);
}
}
}
else
{
CharacterHealth.ServerWrite(msg);
msg.Write(IsRagdolled);
}
}
public void WriteSpawnData(NetBuffer msg)
{
if (GameMain.Server == null) { return; }
msg.Write(Info == null);
msg.Write(ID);
msg.Write(ConfigPath);
msg.Write(seed);
if (Removed)
{
msg.Write(0.0f);
msg.Write(0.0f);
}
else
{
msg.Write(WorldPosition.X);
msg.Write(WorldPosition.Y);
}
msg.Write(Enabled);
//character with no characterinfo (e.g. some monster)
if (Info == null) { return; }
Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
if (ownerClient != null)
{
msg.Write(true);
msg.Write(ownerClient.ID);
}
else if (GameMain.Server.Character == this)
{
msg.Write(true);
msg.Write((byte)0);
}
else
{
msg.Write(false);
}
msg.Write(TeamID);
msg.Write(this is AICharacter);
info.ServerWrite(msg);
}
partial void UpdateNetInput();
}
}
@@ -0,0 +1,187 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class Affliction
{
public readonly AfflictionPrefab Prefab;
public float Strength;
public float DamagePerSecond;
public float DamagePerSecondTimer;
public float PreviousVitalityDecrease;
public float StrengthDiminishMultiplier = 1.0f;
public Affliction MultiplierSource;
/// <summary>
/// Which character gave this affliction
/// </summary>
public Character Source;
public Affliction(AfflictionPrefab prefab, float strength)
{
Prefab = prefab;
Strength = strength;
}
public Affliction CreateMultiplied(float multiplier)
{
return Prefab.Instantiate(Strength * multiplier, Source);
}
public override string ToString()
{
return "Affliction (" + Prefab.Name + ")";
}
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) return 0.0f;
float currVitalityDecrease = MathHelper.Lerp(
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
if (currentEffect.MultiplyByMaxVitality) currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
return currVitalityDecrease;
}
public float GetScreenDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinScreenDistortStrength,
currentEffect.MaxScreenDistortStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetRadialDistortStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinRadialDistortStrength,
currentEffect.MaxRadialDistortStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetChromaticAberrationStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinChromaticAberrationStrength,
currentEffect.MaxChromaticAberrationStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetScreenBlurStrength()
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength <= 0.0f) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinScreenBlurStrength,
currentEffect.MaxScreenBlurStrength,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public void CalculateDamagePerSecond(float currentVitalityDecrease)
{
DamagePerSecond = Math.Max(DamagePerSecond, currentVitalityDecrease - PreviousVitalityDecrease);
if (DamagePerSecondTimer >= 1.0f)
{
DamagePerSecond = currentVitalityDecrease - PreviousVitalityDecrease;
PreviousVitalityDecrease = currentVitalityDecrease;
DamagePerSecondTimer = 0.0f;
}
}
public float GetResistance(string afflictionId)
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxResistance - currentEffect.MinResistance <= 0.0f) return 0.0f;
if (afflictionId != currentEffect.ResistanceFor) return 0.0f;
return MathHelper.Lerp(
currentEffect.MinResistance,
currentEffect.MaxResistance,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public float GetSpeedMultiplier()
{
if (Strength < Prefab.ActivationThreshold) return 1.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 1.0f;
if (currentEffect.MaxSpeedMultiplier - currentEffect.MinSpeedMultiplier <= 0.0f) return 1.0f;
return MathHelper.Lerp(
currentEffect.MinSpeedMultiplier,
currentEffect.MaxSpeedMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return;
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
{
Strength += currentEffect.StrengthChange * deltaTime * StrengthDiminishMultiplier;
}
else // Reduce strengthening of afflictions if resistant
{
Strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier));
}
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets);
}
}
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma
{
class AfflictionBleeding : Affliction
{
public AfflictionBleeding(AfflictionPrefab prefab, float strength) :
base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime;
}
}
}
@@ -1,7 +1,4 @@
#if CLIENT
using Microsoft.Xna.Framework;
#endif
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
@@ -19,7 +16,7 @@ namespace Barotrauma
private InfectionState state;
private Limb huskAppendage;
private List<Limb> huskAppendage;
public InfectionState State
{
@@ -59,30 +56,7 @@ namespace Barotrauma
}
}
private void UpdateMessages(float prevStrength, Character character)
{
#if CLIENT
if (Strength < Prefab.MaxStrength * 0.5f)
{
if (prevStrength % 10.0f > 0.05f && Strength % 10.0f < 0.05f)
{
GUI.AddMessage(TextManager.Get("HuskDormant"), Color.Red);
}
}
else if (Strength < Prefab.MaxStrength)
{
if (state == InfectionState.Dormant && Character.Controlled == character)
{
GUI.AddMessage(TextManager.Get("HuskCantSpeak"), Color.Red);
}
}
else if (state != InfectionState.Active && Character.Controlled == character)
{
GUI.AddMessage(TextManager.Get("HuskActivate").Replace("[Attack]", GameMain.Config.KeyBind(InputType.Attack).ToString()),
Color.Red);
}
#endif
}
partial void UpdateMessages(float prevStrength, Character character);
private void UpdateDormantState(float deltaTime, Character character)
{
@@ -125,45 +99,44 @@ namespace Barotrauma
private void ActivateHusk(Character character)
{
character.NeedsAir = false;
AttachHuskAppendage(character);
if (huskAppendage == null)
{
huskAppendage = AttachHuskAppendage(character);
character.SetStun(0.5f);
}
}
private void AttachHuskAppendage(Character character)
public static List<Limb> AttachHuskAppendage(Character character, Ragdoll ragdoll = null)
{
//husk appendage already created, don't do anything
if (huskAppendage != null) return;
XDocument doc = XMLExtensions.TryLoadXml(Path.Combine("Content", "Characters", "Human", "Huskappendage.xml"));
if (doc == null || doc.Root == null) return;
var limbElement = doc.Root.Element("limb");
if (limbElement == null)
var huskDoc = XMLExtensions.TryLoadXml(Character.GetConfigFile("humanhusk"));
string pathToAppendage = huskDoc.Root.Element("huskappendage").GetAttributeString("path", string.Empty);
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
if (doc == null || doc.Root == null) { return null; }
if (ragdoll == null)
{
DebugConsole.ThrowError("Error in huskappendage.xml - limb element not found");
return;
ragdoll = character.AnimController;
}
var jointElement = doc.Root.Element("joint");
if (jointElement == null)
if (ragdoll.Dir < 1.0f)
{
DebugConsole.ThrowError("Error in huskappendage.xml - joint element not found");
return;
ragdoll.Flip();
}
character.SetStun(0.5f);
if (character.AnimController.Dir < 1.0f)
var huskAppendages = new List<Limb>();
var limbElements = doc.Root.Elements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
foreach (var jointElement in doc.Root.Elements("joint"))
{
character.AnimController.Flip();
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out XElement limbElement))
{
JointParams jointParams = new JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = ragdoll.Limbs[jointParams.Limb1];
Limb huskAppendage = new Limb(ragdoll, character, new LimbParams(limbElement, ragdoll.RagdollParams));
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
huskAppendages.Add(huskAppendage);
}
}
var torso = character.AnimController.GetLimb(LimbType.Torso);
huskAppendage = new Limb(character.AnimController, character, new LimbParams(limbElement, character.AnimController.RagdollParams));
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(torso.SimPosition, torso.Rotation);
character.AnimController.AddLimb(huskAppendage);
character.AnimController.AddJoint(jointElement);
return huskAppendages;
}
private void DeactivateHusk(Character character)
@@ -175,8 +148,7 @@ namespace Barotrauma
private void RemoveHuskAppendage(Character character)
{
if (huskAppendage == null) return;
character.AnimController.RemoveLimb(huskAppendage);
huskAppendage.ForEach(l => character.AnimController.RemoveLimb(l));
huskAppendage = null;
}
@@ -189,7 +161,7 @@ namespace Barotrauma
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
{
if (GameMain.Client != null) { return; }
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (Strength < Prefab.MaxStrength * 0.5f || character.Removed) { return; }
//don't turn the character into a husk if any of its limbs are severed
@@ -210,25 +182,24 @@ namespace Barotrauma
character.Enabled = false;
Entity.Spawner.AddToRemoveQueue(character);
var characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character);
var configFile = characterFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f)?.ToLowerInvariant() == "humanhusk");
var configFile = Character.GetConfigFile("humanhusk");
if (string.IsNullOrEmpty(configFile))
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - humanhusk config file not found.");
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.");
yield return CoroutineStatus.Success;
}
XDocument doc = XMLExtensions.TryLoadXml(configFile);
if (doc?.Root == null)
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - humanhusk config file ("+configFile+") could not be read.");
yield return CoroutineStatus.Success;
}
//XDocument doc = XMLExtensions.TryLoadXml(configFile);
//if (doc?.Root == null)
//{
// DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file ("+configFile+") could not be read.");
// yield return CoroutineStatus.Success;
//}
character.Info.Ragdoll = null;
character.Info.SourceElement = doc.Root;
var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, false, true);
//character.Info.Ragdoll = null;
//character.Info.SourceElement = doc.Root;
var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, isRemotePlayer: false, hasAi: true);
foreach (Limb limb in husk.AnimController.Limbs)
{
@@ -0,0 +1,355 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma
{
public static class CPRSettings
{
public static float ReviveChancePerSkill { get; private set; }
public static float ReviveChanceExponent { get; private set; }
public static float ReviveChanceMin { get; private set; }
public static float ReviveChanceMax { get; private set; }
public static float StabilizationPerSkill { get; private set; }
public static float StabilizationMin { get; private set; }
public static float StabilizationMax { get; private set; }
public static float DamageSkillThreshold { get; private set; }
public static float DamageSkillMultiplier { get; private set; }
public static void Load(XElement element)
{
ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f);
ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f);
ReviveChanceMin = MathHelper.Clamp(element.GetAttributeFloat("revivechancemin", 0.05f), 0.0f, 1.0f);
ReviveChanceMax = MathHelper.Clamp(element.GetAttributeFloat("revivechancemax", 0.9f), ReviveChanceMin, 1.0f);
StabilizationPerSkill = Math.Max(element.GetAttributeFloat("stabilizationperskill", 0.01f), 0.0f);
StabilizationMin = MathHelper.Max(element.GetAttributeFloat("stabilizationmin", 0.05f), 0.0f);
StabilizationMax = MathHelper.Max(element.GetAttributeFloat("stabilizationmax", 2.0f), StabilizationMin);
DamageSkillThreshold = MathHelper.Clamp(element.GetAttributeFloat("damageskillthreshold", 40.0f), 0.0f, 100.0f);
DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f);
}
}
class AfflictionPrefab
{
public class Effect
{
//this effect is applied when the strength is within this range
public float MinStrength, MaxStrength;
public readonly float MinVitalityDecrease = 0.0f;
public readonly float MaxVitalityDecrease = 0.0f;
//how much the strength of the affliction changes per second
public readonly float StrengthChange = 0.0f;
public readonly bool MultiplyByMaxVitality;
public float MinScreenBlurStrength, MaxScreenBlurStrength;
public float MinScreenDistortStrength, MaxScreenDistortStrength;
public float MinRadialDistortStrength, MaxRadialDistortStrength;
public float MinChromaticAberrationStrength, MaxChromaticAberrationStrength;
public float MinSpeedMultiplier, MaxSpeedMultiplier;
public float MinBuffMultiplier, MaxBuffMultiplier;
public float MinResistance, MaxResistance;
public string ResistanceFor;
public string DialogFlag;
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public Effect(XElement element, string parentDebugName)
{
MinStrength = element.GetAttributeFloat("minstrength", 0);
MaxStrength = element.GetAttributeFloat("maxstrength", 0);
MultiplyByMaxVitality = element.GetAttributeBool("multiplybymaxvitality", false);
MinVitalityDecrease = element.GetAttributeFloat("minvitalitydecrease", 0.0f);
MaxVitalityDecrease = element.GetAttributeFloat("maxvitalitydecrease", 0.0f);
MaxVitalityDecrease = Math.Max(MinVitalityDecrease, MaxVitalityDecrease);
MinScreenDistortStrength = element.GetAttributeFloat("minscreendistort", 0.0f);
MaxScreenDistortStrength = element.GetAttributeFloat("maxscreendistort", 0.0f);
MaxScreenDistortStrength = Math.Max(MinScreenDistortStrength, MaxScreenDistortStrength);
MinRadialDistortStrength = element.GetAttributeFloat("minradialdistort", 0.0f);
MaxRadialDistortStrength = element.GetAttributeFloat("maxradialdistort", 0.0f);
MaxRadialDistortStrength = Math.Max(MinRadialDistortStrength, MaxRadialDistortStrength);
MinChromaticAberrationStrength = element.GetAttributeFloat("minchromaticaberration", 0.0f);
MaxChromaticAberrationStrength = element.GetAttributeFloat("maxchromaticaberration", 0.0f);
MaxChromaticAberrationStrength = Math.Max(MinChromaticAberrationStrength, MaxChromaticAberrationStrength);
MinScreenBlurStrength = element.GetAttributeFloat("minscreenblur", 0.0f);
MaxScreenBlurStrength = element.GetAttributeFloat("maxscreenblur", 0.0f);
MaxScreenBlurStrength = Math.Max(MinScreenBlurStrength, MaxScreenBlurStrength);
ResistanceFor = element.GetAttributeString("resistancefor", "");
MinResistance = element.GetAttributeFloat("minresistance", 0.0f);
MaxResistance = element.GetAttributeFloat("maxresistance", 0.0f);
MaxResistance = Math.Max(MinResistance, MaxResistance);
MinSpeedMultiplier = element.GetAttributeFloat("minspeedmultiplier", 1.0f);
MaxSpeedMultiplier = element.GetAttributeFloat("maxspeedmultiplier", 1.0f);
MaxSpeedMultiplier = Math.Max(MinSpeedMultiplier, MaxSpeedMultiplier);
MinBuffMultiplier = element.GetAttributeFloat("minmultiplier", 1.0f);
MaxBuffMultiplier = element.GetAttributeFloat("maxmultiplier", 1.0f);
MaxBuffMultiplier = Math.Max(MinBuffMultiplier, MaxBuffMultiplier);
DialogFlag = element.GetAttributeString("dialogflag", "");
StrengthChange = element.GetAttributeFloat("strengthchange", 0.0f);
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
break;
}
}
}
}
public static AfflictionPrefab InternalDamage;
public static AfflictionPrefab Bleeding;
public static AfflictionPrefab Burn;
public static AfflictionPrefab OxygenLow;
public static AfflictionPrefab Bloodloss;
public static AfflictionPrefab Pressure;
public static AfflictionPrefab Stun;
public static AfflictionPrefab Husk;
public static List<AfflictionPrefab> List = new List<AfflictionPrefab>();
//Arbitrary string that is used to identify the type of the affliction.
//Afflictions with the same type stack up, and items may be configured to cure specific types of afflictions.
public readonly string AfflictionType;
//Does the affliction affect a specific limb or the whole character
public readonly bool LimbSpecific;
//If not a limb-specific affliction, which limb is the indicator shown on in the health menu
//(e.g. mental health problems on head, lack of oxygen on torso...)
public readonly LimbType IndicatorLimb;
public readonly string Identifier;
public readonly string Name, Description;
public readonly bool IsBuff;
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
//how high the strength has to be for the affliction to take affect
public readonly float ActivationThreshold = 0.0f;
//how high the strength has to be for the affliction icon to be shown in the UI
public readonly float ShowIconThreshold = 0.0f;
public readonly float MaxStrength = 100.0f;
public float BurnOverlayAlpha;
public float DamageOverlayAlpha;
//steam achievement given when the affliction is removed from the controlled character
public readonly string AchievementOnRemoved;
public readonly Sprite Icon;
public readonly Color IconColor;
private List<Effect> effects = new List<Effect>();
private Dictionary<string, float> treatmentSuitability = new Dictionary<string, float>();
private readonly string typeName;
private readonly ConstructorInfo constructor;
public Dictionary<string, float> TreatmentSuitability
{
get { return treatmentSuitability; }
}
public static void LoadAll(IEnumerable<string> filePaths)
{
foreach (string filePath in filePaths)
{
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null || doc.Root == null) continue;
foreach (XElement element in doc.Root.Elements())
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "internaldamage":
List.Add(InternalDamage = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "bleeding":
List.Add(Bleeding = new AfflictionPrefab(element, typeof(AfflictionBleeding)));
break;
case "burn":
List.Add(Burn = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "oxygenlow":
List.Add(OxygenLow = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "bloodloss":
List.Add(Bloodloss = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "pressure":
List.Add(Pressure = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "stun":
List.Add(Stun = new AfflictionPrefab(element, typeof(Affliction)));
break;
case "husk":
case "afflictionhusk":
List.Add(Husk = new AfflictionPrefab(element, typeof(AfflictionHusk)));
break;
case "cprsettings":
CPRSettings.Load(element);
break;
default:
List.Add(new AfflictionPrefab(element));
break;
}
}
}
if (InternalDamage == null) DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs.");
if (Bleeding == null) DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs.");
if (Burn == null) DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs.");
if (OxygenLow == null) DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs.");
if (Bloodloss == null) DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs.");
if (Pressure == null) DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs.");
if (Stun == null) DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs.");
if (Husk == null) DebugConsole.ThrowError("Affliction \"Husk\" not defined in the affliction prefabs.");
}
public AfflictionPrefab(XElement element, Type type = null)
{
typeName = type == null ? element.Name.ToString() : type.Name;
Identifier = element.GetAttributeString("identifier", "");
AfflictionType = element.GetAttributeString("type", "");
Name = TextManager.Get("AfflictionName." + Identifier, true) ?? element.GetAttributeString("name", "");
Description = TextManager.Get("AfflictionDescription." + Identifier, true) ?? element.GetAttributeString("description", "");
IsBuff = element.GetAttributeBool("isbuff", false);
LimbSpecific = element.GetAttributeBool("limbspecific", false);
if (!LimbSpecific)
{
string indicatorLimbName = element.GetAttributeString("indicatorlimb", "Torso");
if (!Enum.TryParse(indicatorLimbName, out IndicatorLimb))
{
DebugConsole.ThrowError("Error in affliction prefab " + Name + " - limb type \"" + indicatorLimbName + "\" not found.");
}
}
ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f);
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", ActivationThreshold);
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", "");
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "icon":
Icon = new Sprite(subElement);
IconColor = subElement.GetAttributeColor("color", Color.White);
break;
case "effect":
effects.Add(new Effect(subElement, Name));
break;
}
}
try
{
if (type == null)
{
type = Type.GetType("Barotrauma." + typeName, true, true);
if (type == null)
{
DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\".");
return;
}
}
}
catch
{
DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\".");
return;
}
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
}
public override string ToString()
{
return "AfflictionPrefab (" + Name + ")";
}
public Affliction Instantiate(float strength, Character source = null)
{
object instance = null;
try
{
instance = constructor.Invoke(new object[] { this, strength });
}
catch (Exception ex)
{
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
}
Affliction affliction = instance as Affliction;
affliction.Source = source;
return affliction;
}
public Effect GetActiveEffect(float currentStrength)
{
foreach (Effect effect in effects)
{
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength) return effect;
}
//if above the strength range of all effects, use the highest strength effect
Effect strongestEffect = null;
float largestStrength = currentStrength;
foreach (Effect effect in effects)
{
if (currentStrength > effect.MaxStrength &&
(strongestEffect == null || effect.MaxStrength > largestStrength))
{
strongestEffect = effect;
largestStrength = effect.MaxStrength;
}
}
return strongestEffect;
}
public float GetTreatmentSuitability(Item item)
{
if (item == null || !treatmentSuitability.ContainsKey(item.Prefab.Identifier.ToLowerInvariant()))
{
return 0.0f;
}
return treatmentSuitability[item.Prefab.Identifier.ToLowerInvariant()];
}
}
}
@@ -0,0 +1,27 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Barotrauma.Extensions;
using System.Xml.Linq;
using System;
namespace Barotrauma
{
partial class AfflictionPsychosis : Affliction
{
public AfflictionPsychosis(AfflictionPrefab prefab, float strength) : base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
UpdateProjSpecific(characterHealth, targetLimb, deltaTime);
}
partial void UpdateProjSpecific(CharacterHealth characterHealth, Limb targetLimb, float deltaTime);
}
}
@@ -0,0 +1,53 @@
using Microsoft.Xna.Framework;
namespace Barotrauma
{
class BuffDurationIncrease : Affliction
{
public BuffDurationIncrease(AfflictionPrefab prefab, float strength) : base(prefab, strength)
{
}
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
base.Update(characterHealth, targetLimb, deltaTime);
var afflictions = characterHealth.GetAllAfflictions();
if (Strength <= 0)
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource != this) continue;
affliction.MultiplierSource = null;
affliction.StrengthDiminishMultiplier = 1f;
}
}
else
{
foreach (Affliction affliction in afflictions)
{
if (!affliction.Prefab.IsBuff || affliction == this || affliction.MultiplierSource == this) continue;
float multiplier = GetDiminishMultiplier();
if (affliction.StrengthDiminishMultiplier < multiplier) continue;
affliction.MultiplierSource = this;
affliction.StrengthDiminishMultiplier = multiplier;
}
}
}
private float GetDiminishMultiplier()
{
if (Strength < Prefab.ActivationThreshold) return 1.0f;
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 1.0f;
return MathHelper.Lerp(
currentEffect.MinBuffMultiplier,
currentEffect.MaxBuffMultiplier,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
}
}
}
@@ -74,7 +74,7 @@ namespace Barotrauma
{
return Afflictions.FindAll(a => a.Prefab.AfflictionType == afflictionType);
}
}
}
const float InsufficientOxygenThreshold = 30.0f;
const float LowOxygenThreshold = 50.0f;
@@ -339,7 +339,20 @@ namespace Barotrauma
else
{
AddAffliction(affliction);
}
}
}
public float GetResistance(string resistanceId)
{
float resistance = 0.0f;
for (int i = 0; i < afflictions.Count; i++)
{
if (!afflictions[i].Prefab.IsBuff) continue;
float temp = afflictions[i].GetResistance(resistanceId);
if (temp > resistance) resistance = temp;
}
return resistance;
}
public void ReduceAffliction(Limb targetLimb, string affliction, float amount)
@@ -459,7 +472,7 @@ namespace Barotrauma
{
if (newAffliction.Prefab == affliction.Prefab)
{
affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + newAffliction.Strength * (100.0f / MaxVitality));
affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
affliction.Source = newAffliction.Source;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
@@ -469,11 +482,12 @@ namespace Barotrauma
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
//or modify the affliction instance of an Attack or a StatusEffect
var copyAffliction = newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality)),
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))),
newAffliction.Source);
limbHealth.Afflictions.Add(copyAffliction);
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
@@ -486,11 +500,13 @@ namespace Barotrauma
{
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
// Currently only human can get the husk infection.
if (newAffliction.Prefab == AfflictionPrefab.Husk && Character.SpeciesName.ToLowerInvariant() != "human") { return; }
foreach (Affliction affliction in afflictions)
{
if (newAffliction.Prefab == affliction.Prefab)
{
float newStrength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + newAffliction.Strength * (100.0f / MaxVitality));
float newStrength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + (newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(affliction.Prefab.Identifier))));
if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
affliction.Strength = newStrength;
affliction.Source = newAffliction.Source;
@@ -503,13 +519,17 @@ namespace Barotrauma
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
//or modify the affliction instance of an Attack or a StatusEffect
afflictions.Add(newAffliction.Prefab.Instantiate(
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality)),
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality) * (1f - GetResistance(newAffliction.Prefab.Identifier))),
source: newAffliction.Source));
Character.HealthUpdateInterval = 0.0f;
CalculateVitality();
if (Vitality <= MinVitality) Kill();
}
partial void UpdateLimbAfflictionOverlays();
public void Update(float deltaTime)
{
UpdateOxygen(deltaTime);
@@ -533,39 +553,29 @@ namespace Barotrauma
{
UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime);
}
Character.SpeedMultiplier = affliction.GetSpeedMultiplier();
}
}
for (int i = afflictions.Count - 1; i >= 0; i--)
{
if (irremovableAfflictions.Contains(afflictions[i])) continue;
if (afflictions[i].Strength <= 0.0f)
var affliction = afflictions[i];
if (irremovableAfflictions.Contains(affliction)) continue;
if (affliction.Strength <= 0.0f)
{
SteamAchievementManager.OnAfflictionRemoved(afflictions[i], Character);
SteamAchievementManager.OnAfflictionRemoved(affliction, Character);
afflictions.RemoveAt(i);
}
}
for (int i = 0; i < afflictions.Count; i++)
{
afflictions[i].Update(this, null, deltaTime);
afflictions[i].DamagePerSecondTimer += deltaTime;
var affliction = afflictions[i];
affliction.Update(this, null, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
Character.SpeedMultiplier = affliction.GetSpeedMultiplier();
}
#if CLIENT
foreach (Limb limb in Character.AnimController.Limbs)
{
limb.BurnOverlayStrength = 0.0f;
limb.DamageOverlayStrength = 0.0f;
if (limbHealths[limb.HealthIndex].Afflictions.Count == 0) continue;
foreach (Affliction a in limbHealths[limb.HealthIndex].Afflictions)
{
limb.BurnOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.BurnOverlayAlpha;
limb.DamageOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.DamageOverlayAlpha;
}
limb.BurnOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count;
limb.DamageOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count;
}
#endif
UpdateLimbAfflictionOverlays();
CalculateVitality();
if (Vitality <= MinVitality) Kill();
@@ -598,6 +608,8 @@ namespace Barotrauma
Vitality = MaxVitality;
if (Unkillable) { return; }
float damageResistanceMultiplier = 1f - GetResistance("damage");
foreach (LimbHealth limbHealth in limbHealths)
{
foreach (Affliction affliction in limbHealth.Afflictions)
@@ -611,6 +623,7 @@ namespace Barotrauma
{
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()];
}
vitalityDecrease *= damageResistanceMultiplier;
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
@@ -619,6 +632,7 @@ namespace Barotrauma
foreach (Affliction affliction in afflictions)
{
float vitalityDecrease = affliction.GetVitalityDecrease(this);
vitalityDecrease *= damageResistanceMultiplier;
Vitality -= vitalityDecrease;
affliction.CalculateDamagePerSecond(vitalityDecrease);
}
@@ -627,6 +641,7 @@ namespace Barotrauma
private void Kill()
{
if (Unkillable) { return; }
var causeOfDeath = GetCauseOfDeath();
Character.Kill(causeOfDeath.First, causeOfDeath.Second);
}
@@ -697,7 +712,9 @@ namespace Barotrauma
foreach (Affliction affliction in activeAfflictions)
{
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(affliction.Prefab));
msg.Write(affliction.Strength);
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
}
List<Pair<LimbHealth, Affliction>> limbAfflictions = new List<Pair<LimbHealth, Affliction>>();
@@ -715,7 +732,9 @@ namespace Barotrauma
{
msg.WriteRangedInteger(0, limbHealths.Count - 1, limbHealths.IndexOf(limbAffliction.First));
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(limbAffliction.Second.Prefab));
msg.Write(limbAffliction.Second.Strength);
msg.WriteRangedSingle(
MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength),
0.0f, limbAffliction.Second.Prefab.MaxStrength, 8);
}
}
@@ -3,7 +3,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class DamageModifier
partial class DamageModifier
{
[Serialize(1.0f, false)]
public float DamageMultiplier
@@ -11,7 +11,7 @@ namespace Barotrauma
get;
private set;
}
[Serialize("0.0,360", false)]
public Vector2 ArmorSector
{
@@ -45,17 +45,6 @@ namespace Barotrauma
private set;
}
#if CLIENT
[Serialize("", false)]
public string DamageSound
{
get;
private set;
}
#endif
public DamageModifier(XElement element, string parentDebugName)
{
SerializableProperty.DeserializeProperties(this, element);
@@ -96,6 +96,12 @@ namespace Barotrauma
{
skill.Level += increase;
}
else
{
skills.Add(
skillIdentifier,
new Skill(skillIdentifier, increase));
}
}
public void GiveJobItems(Character character, WayPoint spawnPoint = null)
@@ -134,11 +140,13 @@ namespace Barotrauma
}
Item item = new Item(itemPrefab, character.Position, null);
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
{
Entity.Spawner.CreateNetworkEvent(item, false);
}
#endif
if (itemElement.GetAttributeBool("equip", false))
{
@@ -41,6 +41,13 @@ namespace Barotrauma
private set;
}
[Serialize(false, false)]
public bool OnlyJobSpecificDialog
{
get;
private set;
}
//the number of these characters in the crew the player starts with in the single player campaign
[Serialize(0, false)]
public int InitialCount
@@ -101,12 +108,8 @@ namespace Barotrauma
public JobPrefab(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
string translatedName = TextManager.Get("JobName." + Identifier, true);
if (!string.IsNullOrEmpty(translatedName)) Name = translatedName;
string translatedDescription = TextManager.Get("JobDescription." + Identifier, true);
if (!string.IsNullOrEmpty(translatedDescription)) Description = translatedDescription;
Name = TextManager.Get("JobName." + Identifier);
Description = TextManager.Get("JobDescription." + Identifier);
ItemNames = new List<string>();
@@ -5,21 +5,9 @@ namespace Barotrauma
{
class SkillPrefab
{
private string description;
private Vector2 levelRange;
public readonly string Identifier;
public string Description
{
get { return description; }
}
public Vector2 LevelRange
{
get { return levelRange; }
}
public Vector2 LevelRange { get; private set; }
public SkillPrefab(XElement element)
{
@@ -28,12 +16,12 @@ namespace Barotrauma
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
levelRange = XMLExtensions.ParseVector2(levelString, false);
LevelRange = XMLExtensions.ParseVector2(levelString, false);
}
else
{
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
levelRange = new Vector2(skillLevel, skillLevel);
LevelRange = new Vector2(skillLevel, skillLevel);
}
}
}
@@ -390,17 +390,17 @@ namespace Barotrauma
pullJoint.LocalAnchorA = new Vector2(-pullJoint.LocalAnchorA.X, pullJoint.LocalAnchorA.Y);
}
public AttackResult AddDamage(Vector2 position, float damage, float bleedingDamage, float burnDamage, bool playSound)
public AttackResult AddDamage(Vector2 simPosition, float damage, float bleedingDamage, float burnDamage, bool playSound)
{
List<Affliction> afflictions = new List<Affliction>();
if (damage > 0.0f) afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage));
if (bleedingDamage > 0.0f) afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage));
if (burnDamage > 0.0f) afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage));
return AddDamage(position, afflictions, playSound);
return AddDamage(simPosition, afflictions, playSound);
}
public AttackResult AddDamage(Vector2 position, List<Affliction> afflictions, bool playSound)
public AttackResult AddDamage(Vector2 simPosition, List<Affliction> afflictions, bool playSound)
{
List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
//create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc
@@ -410,7 +410,7 @@ namespace Barotrauma
foreach (DamageModifier damageModifier in damageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
if (SectorHit(damageModifier.ArmorSector, position))
if (SectorHit(damageModifier.ArmorSector, simPosition))
{
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
@@ -422,7 +422,7 @@ namespace Barotrauma
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
{
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
if (SectorHit(damageModifier.ArmorSector, position))
if (SectorHit(damageModifier.ArmorSector, simPosition))
{
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
@@ -431,39 +431,51 @@ namespace Barotrauma
}
}
AddDamageProjSpecific(position, afflictions, playSound, appliedDamageModifiers);
AddDamageProjSpecific(simPosition, afflictions, playSound, appliedDamageModifiers);
return new AttackResult(afflictions, this, appliedDamageModifiers);
}
partial void AddDamageProjSpecific(Vector2 position, List<Affliction> afflictions, bool playSound, List<DamageModifier> appliedDamageModifiers);
partial void AddDamageProjSpecific(Vector2 simPosition, List<Affliction> afflictions, bool playSound, List<DamageModifier> appliedDamageModifiers);
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
{
if (armorSector == Vector2.Zero) return false;
float rot = body.Rotation;
if (Dir == -1) rot -= MathHelper.Pi;
if (armorSector == Vector2.Zero) { return false; }
Vector2 armorLimits = new Vector2(rot - armorSector.X * Dir, rot - armorSector.Y * Dir);
// Can't get this to work properly.
//float rot = body.Rotation;
//if (Dir == -1) { rot -= MathHelper.Pi; }
//Vector2 armorLimits = new Vector2(rot - armorSector.X * Dir, rot - armorSector.Y * Dir);
//float mid = (armorLimits.X + armorLimits.Y) / 2;
//float angleDiff = MathUtils.GetShortestAngle(MathUtils.VectorToAngle(simPosition - SimPosition), mid);
//return (Math.Abs(angleDiff) < (armorSector.Y - armorSector.X) / 2);
float mid = (armorLimits.X + armorLimits.Y) / 2.0f;
float angleDiff = MathUtils.GetShortestAngle(MathUtils.VectorToAngle(simPosition - SimPosition), mid);
// Alternative implementation
float offset = GetArmorSectorRotationOffset(armorSector, body.Rotation);
Vector2 forward = Vector2.Transform(-Vector2.UnitY, Matrix.CreateRotationZ(offset));
float hitAngle = VectorExtensions.Angle(forward, simPosition - SimPosition);
float sectorSize = MathHelper.ToDegrees(GetArmorSectorSize(armorSector));
return hitAngle < sectorSize / 2;
}
return (Math.Abs(angleDiff) < (armorSector.Y - armorSector.X) / 2.0f);
protected float GetArmorSectorRotationOffset(Vector2 armorSector, float bodyRotation)
{
float midAngle = MathUtils.GetMidAngle(armorSector.X, armorSector.Y);
float spritesheetOrientation = MathHelper.ToRadians(limbParams.Ragdoll.SpritesheetOrientation);
return bodyRotation + (midAngle + spritesheetOrientation) * Dir;
}
protected float GetArmorSectorSize(Vector2 armorSector)
{
float min = Math.Min(armorSector.X, armorSector.Y);
float max = Math.Max(armorSector.X, armorSector.Y);
return max - min;
}
public void Update(float deltaTime)
{
UpdateProjSpecific(deltaTime);
if (LinearVelocity.X > 500.0f)
{
//DebugConsole.ThrowError("CHARACTER EXPLODED");
body.ResetDynamics();
body.SetTransform(character.SimPosition, 0.0f);
}
if (inWater)
{
body.ApplyWaterForces();
@@ -512,9 +524,10 @@ namespace Barotrauma
SimPosition, attackPosition,
ignoredBodies, Physics.CollisionWall);
if (damageTarget is Item && structureBody?.UserData is Item)
if (damageTarget is Item)
{
// If the attack is aimed to an item and hits an item, it's successful
// If the attack is aimed to an item and hits an item, it's successful.
// Ignore blocking on items, because it causes cases where a Mudraptor cannot hit the hatch, for example.
wasHit = true;
}
else if (damageTarget is Structure && structureBody?.UserData is Structure)