Unstable 0.1500.6.0 (1984 edition)

This commit is contained in:
Markus Isberg
2021-10-09 00:22:02 +09:00
parent 08bdfc6cea
commit c8943ef9c4
96 changed files with 1817 additions and 559 deletions
@@ -105,7 +105,6 @@ namespace Barotrauma
protected readonly float colliderWidth;
protected readonly float minGapSize;
protected readonly float minHullSize;
protected readonly float colliderLength;
protected readonly float avoidLookAheadDistance;
@@ -119,7 +118,6 @@ namespace Barotrauma
colliderLength = size.Y;
avoidLookAheadDistance = Math.Max(Math.Max(colliderWidth, colliderLength) * 3, 1.5f);
minGapSize = ConvertUnits.ToDisplayUnits(Math.Min(colliderWidth, colliderLength));
minHullSize = ConvertUnits.ToDisplayUnits(Math.Max(colliderLength, colliderWidth) * 1.1f);
}
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
@@ -148,6 +148,8 @@ namespace Barotrauma
private CirclePhase CirclePhase;
private float currentAttackIntensity;
private CoroutineHandle disableTailCoroutine;
private readonly IEnumerable<Body> myBodies;
public LatchOntoAI LatchOntoAI { get; private set; }
@@ -556,7 +558,7 @@ namespace Barotrauma
}
else
{
if (Character.Submarine != null)
if (Character.Submarine != null && Character.Params.UsePathFinding)
{
if (steeringManager != insideSteering)
{
@@ -678,8 +680,21 @@ namespace Barotrauma
}
}
float sqrDist = Vector2.DistanceSquared(WorldPosition, SelectedAiTarget.WorldPosition);
float reactDist = selectedTargetingParams != null && selectedTargetingParams.ReactDistance > 0 ? selectedTargetingParams.ReactDistance : GetPerceivingRange(SelectedAiTarget);
if (sqrDist > Math.Pow(reactDist + movementMargin, 2))
float reactDist = GetPerceivingRange(SelectedAiTarget);
Vector2 offset = Vector2.Zero;
if (selectedTargetingParams != null)
{
if (selectedTargetingParams.ReactDistance > 0)
{
reactDist = selectedTargetingParams.ReactDistance;
}
offset = selectedTargetingParams.Offset;
}
if (offset != Vector2.Zero)
{
reactDist += offset.Length();
}
if (sqrDist > MathUtils.Pow2(reactDist + movementMargin))
{
movementMargin = State == AIState.FleeTo ? 0 : reactDist;
run = true;
@@ -692,17 +707,43 @@ namespace Barotrauma
{
SteeringManager.Reset();
Character.AnimController.TargetMovement = Vector2.Zero;
float force = Character.AnimController.SwimSlowParams.SteerTorque;
Character.AnimController.Collider.MoveToPos(SelectedAiTarget.Entity.SimPosition, force);
if (SelectedAiTarget.Entity is Item item)
if (Character.AnimController.InWater)
{
Character.AnimController.Collider.SmoothRotate(MathHelper.ToRadians(item.Rotation), force);
float force = Character.AnimController.Collider.Mass / 10;
Character.AnimController.Collider.MoveToPos(SelectedAiTarget.Entity.SimPosition + ConvertUnits.ToSimUnits(offset), force);
if (SelectedAiTarget.Entity is Item item)
{
float rotation = item.Rotation;
Character.AnimController.Collider.SmoothRotate(rotation, Character.AnimController.SwimFastParams.SteerTorque);
var mainLimb = Character.AnimController.MainLimb;
if (mainLimb.type == LimbType.Head)
{
mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.HeadTorque);
}
else
{
mainLimb.body.SmoothRotate(rotation, Character.AnimController.SwimFastParams.TorsoTorque);
}
}
if (disableTailCoroutine == null && SelectedAiTarget.Entity is Item i && i.HasTag("guardianshelter"))
{
if (!CoroutineManager.IsCoroutineRunning(disableTailCoroutine))
{
disableTailCoroutine = CoroutineManager.Invoke(() =>
{
if (Character != null && !Character.Removed)
{
Character.AnimController.HideAndDisable(LimbType.Tail, ignoreCollisions: false);
}
}, 1f);
}
}
Character.AnimController.ApplyPose(
new Vector2(0, -1),
new Vector2(0, -1),
new Vector2(0, -1),
new Vector2(0, -1), footMoveForce: 1);
}
Character.AnimController.ApplyPose(
new Vector2(0, -1),
new Vector2(0, -1),
new Vector2(0, -1),
new Vector2(0, -1), footMoveForce: 1);
}
else
{
@@ -895,7 +936,7 @@ namespace Barotrauma
else if (targetHulls.Any())
{
patrolTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
var path = PathSteering.PathFinder.FindPath(Character.SimPosition, patrolTarget.SimPosition, minGapSize: minGapSize * 1.5f, nodeFilter: n => NodeFilter(n) && PatrolNodeFilter(n));
var path = PathSteering.PathFinder.FindPath(Character.SimPosition, patrolTarget.SimPosition, minGapSize: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n));
if (path.Unreachable)
{
@@ -926,7 +967,7 @@ namespace Barotrauma
}
if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable)
{
PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => NodeFilter(n) && PatrolNodeFilter(n));
PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n));
return;
}
}
@@ -938,8 +979,6 @@ namespace Barotrauma
UpdateIdle(deltaTime, followLastTarget);
}
private bool NodeFilter(PathNode n) => n.Waypoint.CurrentHull == null || n.Waypoint.CurrentHull.Rect.Width > minHullSize && n.Waypoint.CurrentHull.Rect.Height > minHullSize;
private void FindTargetHulls()
{
if (Character.Submarine == null) { return; }
@@ -1479,8 +1518,11 @@ namespace Barotrauma
if (!canAttack || distance > margin)
{
// Steer towards the target if in the same room and swimming
if (Character.CurrentHull != null && ((Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))))
// Ruins have walls/pillars inside hulls and therefore we should navigate around them using the path steering.
if (Character.CurrentHull != null &&
Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
(Character.AnimController.InWater || pursue || !Character.AnimController.CanWalk) &&
targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull))
{
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
@@ -1489,8 +1531,7 @@ namespace Barotrauma
{
pathSteering.SteeringSeek(steerPos, weight: 2,
minGapWidth: minGapSize,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null),
nodeFilter: NodeFilter,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null),
checkVisiblity: true);
if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
@@ -1526,7 +1567,7 @@ namespace Barotrauma
}
else
{
pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize, NodeFilter);
pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize);
}
}
else
@@ -1750,7 +1791,7 @@ namespace Barotrauma
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize, NodeFilter);
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
@@ -1762,7 +1803,7 @@ namespace Barotrauma
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
if (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2))
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
@@ -2282,12 +2323,12 @@ namespace Barotrauma
State = AIState.Idle;
return;
}
if (Character.CurrentHull != null && PathSteering != null)
if (Character.CurrentHull != null && steeringManager == insideSteering)
{
// Inside
Character targetCharacter = SelectedAiTarget.Entity as Character;
// Inside, but not inside ruins
if ((Character.AnimController.InWater || !Character.AnimController.CanWalk) &&
(targetCharacter != null && VisibleHulls.Contains(targetCharacter.CurrentHull) || Character.CanSeeTarget(SelectedAiTarget.Entity)))
Character.Submarine != null && !Character.Submarine.Info.IsRuin &&
SelectedAiTarget.Entity is Character c && VisibleHulls.Contains(c.CurrentHull))
{
// Steer towards the target if in the same room and swimming
Vector2 dir = Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition);
@@ -2299,11 +2340,12 @@ namespace Barotrauma
else
{
// Use path finding
PathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize, NodeFilter);
PathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2, minGapWidth: minGapSize);
if (!PathSteering.IsPathDirty && PathSteering.CurrentPath.Unreachable)
{
// Can't reach
State = AIState.Idle;
IgnoreTarget(SelectedAiTarget);
return;
}
}
@@ -2419,10 +2461,12 @@ namespace Barotrauma
}
else
{
// Ignore all structures, items, and hulls inside wrecks and beacons
// Ignore all structures, items, and hulls inside these subs.
if (aiTarget.Entity.Submarine != null)
{
if (aiTarget.Entity.Submarine.Info.IsWreck || aiTarget.Entity.Submarine.Info.IsBeacon || UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
if (aiTarget.Entity.Submarine.Info.IsWreck ||
aiTarget.Entity.Submarine.Info.IsBeacon ||
UnattackableSubmarines.Contains(aiTarget.Entity.Submarine))
{
continue;
}
@@ -2433,6 +2477,7 @@ namespace Barotrauma
if (character.CurrentHull != null) { continue; }
// Ignore ruins
if (hull.Submarine == null) { continue; }
if (hull.Submarine.Info.IsRuin) { continue; }
}
Door door = null;
@@ -2448,14 +2493,6 @@ namespace Barotrauma
continue;
}
}
if (door == null)
{
// Ignore items inside ruins, unless we are in the same hull. We can't target the ruin walls.
if (item.Submarine == null && item.CurrentHull != Character.CurrentHull)
{
continue;
}
}
foreach (var prio in AIParams.Targets)
{
if (item.HasTag(prio.Tag))
@@ -2499,6 +2536,7 @@ namespace Barotrauma
}
if (s.IsPlatform) { continue; }
if (s.Submarine == null) { continue; }
if (s.Submarine.Info.IsRuin) { continue; }
bool isCharacterInside = character.CurrentHull != null;
bool isInnerWall = s.prefab.Tags.Contains("inner");
if (isInnerWall && !isCharacterInside)
@@ -2709,7 +2747,11 @@ namespace Barotrauma
{
dist *= 0.9f;
}
if (!CanPerceive(aiTarget, dist)) { continue; }
if (!CanPerceive(aiTarget, dist, checkVisibility: SelectedAiTarget != aiTarget))
{
continue;
}
if (SelectedAiTarget == aiTarget)
{
@@ -2720,7 +2762,6 @@ namespace Barotrauma
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and attack whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
{
@@ -2907,6 +2948,7 @@ namespace Barotrauma
wallTarget = null;
if (SelectedAiTarget == null) { return; }
if (SelectedAiTarget.Entity == null) { return; }
if (!canAttackWalls) { return; }
if (HasValidPath(requireNonDirty: true)) { return; }
wallHits.Clear();
Structure wall = null;
@@ -3129,7 +3171,7 @@ namespace Barotrauma
{
_selectedAiTarget = null;
}
else if (CanPerceive(_selectedAiTarget))
else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
{
var memory = GetTargetMemory(_selectedAiTarget, false);
if (memory != null)
@@ -3367,6 +3409,12 @@ namespace Barotrauma
protected override void OnStateChanged(AIState from, AIState to)
{
LatchOntoAI?.DeattachFromBody(reset: true);
if (disableTailCoroutine != null)
{
CoroutineManager.StopCoroutines(disableTailCoroutine);
Character.AnimController.RestoreTemporarilyDisabled();
disableTailCoroutine = null;
}
Character.AnimController.ReleaseStuckLimbs();
AttackingLimb = null;
movementMargin = 0;
@@ -3382,11 +3430,16 @@ namespace Barotrauma
private float GetPerceivingRange(AITarget target) => Math.Max(target.SightRange * Sight, target.SoundRange * Hearing);
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1)
private bool CanPerceive(AITarget target, float dist = -1, float distSquared = -1, bool checkVisibility = false)
{
bool insideSightRange;
bool insideSoundRange;
checkVisibility = checkVisibility && Character.Submarine != null && target.Entity.Submarine == Character.Submarine;
if (dist > 0)
{
return dist <= target.SightRange * Sight || dist <= target.SoundRange * Hearing;
insideSightRange = IsInRange(dist, target.SightRange, Sight);
if (!checkVisibility && insideSightRange) { return true; }
insideSoundRange = IsInRange(dist, target.SoundRange, Hearing);
}
else
{
@@ -3394,8 +3447,42 @@ namespace Barotrauma
{
distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
}
return distSquared <= MathUtils.Pow(target.SightRange * Sight, 2) || distSquared <= MathUtils.Pow(target.SoundRange * Hearing, 2);
insideSightRange = IsInRangeSqr(distSquared, target.SightRange, Sight);
if (!checkVisibility && insideSightRange) { return true; }
insideSoundRange = IsInRangeSqr(distSquared, target.SoundRange, Hearing);
}
if (!checkVisibility)
{
return insideSightRange || insideSoundRange;
}
else
{
if (!insideSightRange && !insideSoundRange) { return false; }
// Inside the same submarine -> check whether the target is behind a wall
if (target.Entity is Character c && VisibleHulls.Contains(c.CurrentHull) || target.Entity is Item i && VisibleHulls.Contains(i.CurrentHull))
{
return insideSightRange || insideSoundRange;
}
else
{
// No line of sight to the target -> Ignore sight and use only half of the sound range
if (dist > 0)
{
return IsInRange(dist, target.SoundRange, Hearing / 2);
}
else
{
if (distSquared < 0)
{
distSquared = Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition);
}
return IsInRangeSqr(distSquared, target.SoundRange, Hearing / 2);
}
}
}
bool IsInRange(float dist, float range, float perception) => dist <= range * perception;
bool IsInRangeSqr(float distSquared, float range, float perception) => distSquared <= MathUtils.Pow2(range * perception);
}
public void ReevaluateAttacks()
@@ -34,9 +34,6 @@ namespace Barotrauma
private float flipTimer;
private const float FlipInterval = 0.5f;
private float teamChangeTimer;
private const float TeamChangeInterval = 0.5f;
public const float HULL_SAFETY_THRESHOLD = 40;
public const float HULL_LOW_OXYGEN_PERCENTAGE = 30;
@@ -1124,7 +1121,7 @@ namespace Barotrauma
{
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
// Inform other NPCs
if (cumulativeDamage > 1)
if (cumulativeDamage > 1 || totalDamage >= 10)
{
InformOtherNPCs(cumulativeDamage);
}
@@ -26,7 +26,7 @@ namespace Barotrauma
public bool AttachToWalls { get; private set; }
public bool AttachToCharacters { get; private set; }
private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration;
private readonly float minDeattachSpeed, maxDeattachSpeed, maxAttachDuration, coolDown;
private readonly float damageOnDetach, detachStun;
private readonly bool weld;
private float deattachCheckTimer;
@@ -61,6 +61,7 @@ namespace Barotrauma
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 5.0f);
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 8.0f));
maxAttachDuration = element.GetAttributeFloat("maxattachduration", -1.0f);
coolDown = element.GetAttributeFloat("cooldown", 2f);
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
@@ -283,6 +284,7 @@ namespace Barotrauma
if (maxAttachDuration > 0)
{
deattach = true;
attachCooldown = coolDown;
}
if (!deattach && targetWall != null && targetSubmarine != null)
{
@@ -291,7 +293,7 @@ namespace Barotrauma
if (enemyAI.CanPassThroughHole(targetWall, targetSection))
{
deattach = true;
attachCooldown = 2;
attachCooldown = coolDown;
}
if (!deattach)
{
@@ -310,7 +312,7 @@ namespace Barotrauma
{
deattach = true;
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
attachCooldown = detachStun * 2;
attachCooldown = Math.Max(detachStun * 2, coolDown);
}
}
}
@@ -1048,6 +1048,7 @@ namespace Barotrauma
if (closeEnough)
{
UseWeapon(deltaTime);
character.AIController.SteeringManager.Reset();
}
else if (!character.IsFacing(Enemy.WorldPosition))
{
@@ -163,7 +163,7 @@ namespace Barotrauma
CoroutineManager.StopCoroutines(coroutine);
DelayedObjectives.Remove(objective);
}
coroutine = CoroutineManager.InvokeAfter(() =>
coroutine = CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
#if CLIENT
@@ -357,8 +357,11 @@ namespace Barotrauma
float itemAngle;
Holdable holdable = item.GetComponent<Holdable>();
float torsoRotation = torso.Rotation;
bool equippedInRightHand = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == item && rightHand != null && !rightHand.IsSevered;
bool equippedInLefthand = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == item && leftHand != null && !leftHand.IsSevered;
Item rightHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
bool equippedInRightHand = rightHandItem == item && rightHand != null && !rightHand.IsSevered;
Item leftHandItem = character.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
bool equippedInLefthand = leftHandItem == item && leftHand != null && !leftHand.IsSevered;
if (aim && !isClimbing && !usingController && character.Stun <= 0.0f && itemPos != Vector2.Zero && !character.IsIncapacitated)
{
Vector2 mousePos = ConvertUnits.ToSimUnits(character.SmoothedCursorPosition);
@@ -366,17 +369,23 @@ namespace Barotrauma
holdAngle = MathUtils.VectorToAngle(new Vector2(diff.X, diff.Y * Dir)) - torsoRotation * Dir;
holdAngle += GetAimWobble(rightHand, leftHand, item);
itemAngle = torsoRotation + holdAngle * Dir;
if (holdable.ControlPose)
{
var head = GetLimb(LimbType.Head);
if (head != null)
//if holding two items that should control the characters' pose, let the item in the right hand do it
bool anotherItemControlsPose = equippedInLefthand && rightHandItem != item && (rightHandItem?.GetComponent<Holdable>()?.ControlPose ?? false);
if (!anotherItemControlsPose)
{
head.body.SmoothRotate(itemAngle, force: 30 * head.Mass);
}
if (TargetMovement == Vector2.Zero && inWater)
{
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
var head = GetLimb(LimbType.Head);
if (head != null)
{
head.body.SmoothRotate(itemAngle, force: 30 * head.Mass);
}
if (TargetMovement == Vector2.Zero && inWater)
{
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
}
}
aiming = true;
}
@@ -506,7 +515,11 @@ namespace Barotrauma
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)");
}
#endif
HandIK(i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i], CurrentAnimationParams.ArmIKStrength, CurrentAnimationParams.HandIKStrength);
HandIK(
i == 0 ? rightHand : leftHand, transformedHoldPos + transformedHandlePos[i],
CurrentAnimationParams.ArmIKStrength,
CurrentAnimationParams.HandIKStrength,
maxAngularVelocity: 15.0f);
}
}
}
@@ -531,7 +544,7 @@ namespace Barotrauma
return (lowFreqNoise * 1.0f + highFreqNoise * 0.1f) * wobbleStrength;
}
public void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f)
public void HandIK(Limb hand, Vector2 pos, float armTorque = 1.0f, float handTorque = 1.0f, float maxAngularVelocity = float.PositiveInfinity)
{
Vector2 shoulderPos;
@@ -572,11 +585,20 @@ namespace Barotrauma
armAngle -= MathHelper.TwoPi;
}
arm?.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false);
if (arm?.body != null && Math.Abs(arm.body.AngularVelocity) < maxAngularVelocity)
{
arm.body.SmoothRotate(armAngle - upperArmAngle, 100.0f * armTorque * arm.Mass, wrapAngle: false);
}
float forearmAngle = armAngle + lowerArmAngle;
forearm?.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false);
float handAngle = forearm != null ? forearmAngle : armAngle;
hand?.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false);
if (forearm?.body != null && Math.Abs(forearm.body.AngularVelocity) < maxAngularVelocity)
{
forearm.body.SmoothRotate(forearmAngle, 100.0f * handTorque * forearm.Mass, wrapAngle: false);
}
if (hand?.body != null && Math.Abs(hand.body.AngularVelocity) < maxAngularVelocity)
{
float handAngle = forearm != null ? forearmAngle : armAngle;
hand.body.SmoothRotate(handAngle, 10.0f * handTorque * hand.Mass, wrapAngle: false);
}
}
public void ApplyPose(Vector2 leftHandPos, Vector2 rightHandPos, Vector2 leftFootPos, Vector2 rightFootPos, float footMoveForce = 10)
@@ -672,7 +694,7 @@ namespace Barotrauma
forearmLength += Vector2.Distance(
rightHand.PullJointLocalAnchorA,
rightElbow.LimbA.type == LimbType.RightHand ? rightElbow.LocalAnchorA : rightElbow.LocalAnchorB);
rightWrist.LimbA.type == LimbType.RightHand ? rightWrist.LocalAnchorA : rightWrist.LocalAnchorB);
}
}
}
@@ -815,7 +815,7 @@ namespace Barotrauma
{
limb.body.SmoothRotate(MainLimb.Rotation + MathHelper.ToRadians(limb.Params.ConstantAngle) * Dir, limb.Mass * limb.Params.ConstantTorque, wrapAngle: true);
}
if (limb.Params.BlinkFrequency > 0)
if (limb.Params.BlinkFrequency > 0 && !limb.Params.OnlyBlinkInWater)
{
limb.UpdateBlink(deltaTime, MainLimb.Rotation);
}
@@ -170,7 +170,7 @@ namespace Barotrauma
{
get
{
float shoulderHeight = Collider.height / 2.0f - 0.1f;
float shoulderHeight = Collider.height / 2.0f;
if (inWater)
{
shoulderHeight += 0.4f;
@@ -987,7 +987,7 @@ namespace Barotrauma
return;
}
handPos += head.LinearVelocity * 0.1f;
handPos += head.LinearVelocity.ClampLength(1.0f) * 0.1f;
// Not sure why the params has to be flipped, but it works.
var handMoveAmount = CurrentSwimParams.HandMoveAmount.Flip();
@@ -1002,7 +1002,7 @@ namespace Barotrauma
Vector2 rightHandPos = new Vector2(-handPosX, -handPosY) + handMoveOffset;
rightHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, rightHandPos.X) : Math.Min(-0.3f, rightHandPos.X);
rightHandPos = Vector2.Transform(rightHandPos, rotationMatrix);
float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetRightHandPenalty());
float speedMultiplier = Math.Min(character.SpeedMultiplier * (1 - Character.GetRightHandPenalty()), 1.0f);
// Limb hand, Vector2 pos, float force = 1.0f
HandIK(rightHand, handPos + rightHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier);
}
@@ -1012,7 +1012,7 @@ namespace Barotrauma
Vector2 leftHandPos = new Vector2(handPosX, handPosY) + handMoveOffset;
leftHandPos.X = (Dir == 1.0f) ? Math.Max(0.3f, leftHandPos.X) : Math.Min(-0.3f, leftHandPos.X);
leftHandPos = Vector2.Transform(leftHandPos, rotationMatrix);
float speedMultiplier = character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty());
float speedMultiplier = Math.Min(character.SpeedMultiplier * (1 - Character.GetLeftHandPenalty()), 1.0f);
HandIK(leftHand, handPos + leftHandPos, CurrentSwimParams.ArmMoveStrength * speedMultiplier, CurrentSwimParams.HandMoveStrength * speedMultiplier);
}
}
@@ -1212,24 +1212,24 @@ namespace Barotrauma
//the room where the ragdoll is in is used as the "guess", meaning that it's checked first
Hull limbHull = currentHull == null ? null : Hull.FindHull(limb.WorldPosition, currentHull);
bool prevInWater = limb.inWater;
limb.inWater = false;
bool prevInWater = limb.InWater;
limb.InWater = false;
if (forceStanding)
{
limb.inWater = false;
limb.InWater = false;
}
else if (limbHull == null)
{
//limb isn't in any room -> it's in the water
limb.inWater = true;
limb.InWater = true;
if (limb.type == LimbType.Head) headInWater = true;
}
else if (limbHull.WaterVolume > 0.0f && Submarine.RectContains(limbHull.Rect, limb.Position))
{
if (limb.Position.Y < limbHull.Surface)
{
limb.inWater = true;
limb.InWater = true;
surfaceY = limbHull.Surface;
if (limb.type == LimbType.Head)
{
@@ -1237,7 +1237,7 @@ namespace Barotrauma
}
}
//the limb has gone through the surface of the water
if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.inWater != prevInWater)
if (Math.Abs(limb.LinearVelocity.Y) > 5.0f && limb.InWater != prevInWater)
{
Splash(limb, limbHull);
@@ -1435,7 +1435,7 @@ namespace Barotrauma
flowForce *= 1 - Math.Clamp(character.GetStatValue(StatTypes.FlowResistance), 0f, 1f);
float flowForceMagnitude = flowForce.Length();
float limbMultipier = limbs.Count(l => l.inWater) / (float)limbs.Length;
float limbMultipier = limbs.Count(l => l.InWater) / (float)limbs.Length;
//if the force strong enough, stun the character to let it get thrown around by the water
if ((flowForceMagnitude * limbMultipier) - flowStunTolerance > StunForceThreshold)
{
@@ -1472,7 +1472,7 @@ namespace Barotrauma
Collider.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
foreach (Limb limb in limbs)
{
if (!limb.inWater) { continue; }
if (!limb.InWater) { continue; }
limb.body.ApplyForce(flowForce, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
@@ -1840,9 +1840,23 @@ namespace Barotrauma
public void ReleaseStuckLimbs()
{
Limbs.ForEach(l => l.Release());
// Commented out, because stuck limbs is not a feature that we currently use, as it would require that we sync all the limbs, which we don't do.
//Limbs.ForEach(l => l.Release());
}
public void HideAndDisable(LimbType limbType, float duration = 0, bool ignoreCollisions = true)
{
foreach (var limb in Limbs)
{
if (limb.type == limbType)
{
limb.HideAndDisable(duration, ignoreCollisions);
}
}
}
public void RestoreTemporarilyDisabled() => Limbs.ForEach(l => l.ReEnable());
public void Remove()
{
if (Limbs != null)
@@ -313,6 +313,15 @@ namespace Barotrauma
public string TraitorCurrentObjective = "";
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// Can be used by status effects to check the character's gender
/// </summary>
public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male;
/// <summary>
/// Can be used by status effects to check the character's gender
/// </summary>
public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female;
private float attackCoolDown;
public List<OrderInfo> CurrentOrders => Info?.CurrentOrders;
@@ -581,6 +590,14 @@ namespace Barotrauma
}
}
/// <summary>
/// Can be used by status effects to check whether the characters is in a high-pressure environment
/// </summary>
public bool InPressure
{
get { return CurrentHull == null || CurrentHull.LethalPressure > 5.0f; }
}
public const float KnockbackCooldown = 5.0f;
public float KnockbackCooldownTimer;
@@ -641,22 +658,13 @@ namespace Barotrauma
public bool DisableHealthWindow;
public float Vitality
{
get { return CharacterHealth.Vitality; }
}
public float Health
{
get { return CharacterHealth.Vitality; }
}
// These properties needs to be exposed for status effects
public float Vitality => CharacterHealth.Vitality;
public float Health => Vitality;
public float HealthPercentage => CharacterHealth.HealthPercentage;
public float MaxVitality
{
get { return CharacterHealth.MaxVitality; }
}
public float MaxVitality => CharacterHealth.MaxVitality;
public float MaxHealth => MaxVitality;
public AIState AIState => AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public float Bloodloss
{
@@ -2405,7 +2413,7 @@ namespace Barotrauma
var head = AnimController.GetLimb(LimbType.Head);
bool headInWater = head == null ?
AnimController.InWater :
head.inWater;
head.InWater;
//climb ladders automatically when pressing up/down inside their trigger area
Ladder currentLadder = SelectedConstruction?.GetComponent<Ladder>();
if ((SelectedConstruction == null || currentLadder != null) &&
@@ -3813,6 +3821,7 @@ namespace Barotrauma
foreach (var joint in AnimController.LimbJoints)
{
if (joint.LimbA.type == LimbType.Head || joint.LimbB.type == LimbType.Head) { continue; }
if (joint.revoluteJoint != null)
{
joint.revoluteJoint.LimitEnabled = false;
@@ -3950,7 +3959,10 @@ namespace Barotrauma
foreach (Limb limb in AnimController.Limbs)
{
#if CLIENT
if (limb.LightSource != null) limb.LightSource.Color = limb.InitialLightSourceColor;
if (limb.LightSource != null)
{
limb.LightSource.Color = limb.InitialLightSourceColor;
}
#endif
limb.body.Enabled = true;
limb.IsSevered = false;
@@ -4369,7 +4381,6 @@ namespace Barotrauma
if (!info.UnlockedTalents.Add(talentPrefab.Identifier)) { return false; }
}
DebugConsole.AddWarning("added " + talentPrefab.OriginalName);
CharacterTalent characterTalent = new CharacterTalent(talentPrefab, this);
characterTalent.ActivateTalent(addingFirstTime);
characterTalents.Add(characterTalent);
@@ -4377,7 +4388,10 @@ namespace Barotrauma
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.UpdateTalents });
#endif
if (addingFirstTime)
{
OnTalentGiven(talentPrefab.Identifier);
}
return true;
}
@@ -4441,6 +4455,7 @@ namespace Barotrauma
}
partial void OnMoneyChanged(int prevAmount, int newAmount);
partial void OnTalentGiven(string talentIdentifier);
/// <summary>
/// This dictionary is used for stats that are required very frequently. Not very performant, but easier to develop with for now.
@@ -451,9 +451,9 @@ namespace Barotrauma
set => Head.FaceAttachmentIndex = value;
}
public readonly ImmutableArray<Color> HairColors;
public readonly ImmutableArray<Color> FacialHairColors;
public readonly ImmutableArray<Color> SkinColors;
public readonly ImmutableArray<(Color Color, float Commonness)> HairColors;
public readonly ImmutableArray<(Color Color, float Commonness)> FacialHairColors;
public readonly ImmutableArray<(Color Color, float Commonness)> SkinColors;
public Color HairColor
{
@@ -522,15 +522,12 @@ namespace Barotrauma
// TODO: support for variants
Head = new HeadInfo();
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
Head.gender = GetRandomGender(randSync);
HasRaces = CharacterConfigElement.GetAttributeBool("races", false);
Head.race = GetRandomRace(randSync);
CalculateHeadSpriteRange();
HeadSpriteId = GetRandomHeadID(randSync);
SetGenderAndRace(randSync);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant);
HairColors = CharacterConfigElement.GetAttributeColorArray("haircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeColorArray("facialhaircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeColorArray("skincolors", new Color[] { new Color(255, 215, 200, 255) }).ToImmutableArray();
HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
SetColors();
if (!string.IsNullOrEmpty(name))
@@ -580,26 +577,38 @@ namespace Barotrauma
return name;
}
public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array)
=> ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), Rand.RandSync.Unsynced)
.Color;
private void SetGenderAndRace(Rand.RandSync randSync)
{
Head.gender = GetRandomGender(randSync);
Head.race = GetRandomRace(randSync);
CalculateHeadSpriteRange();
HeadSpriteId = GetRandomHeadID(randSync);
}
private void SetColors()
{
HairColor = HairColors.GetRandom();
FacialHairColor = FacialHairColors.GetRandom();
SkinColor = SkinColors.GetRandom();
HairColor = SelectRandomColor(HairColors);
FacialHairColor = SelectRandomColor(FacialHairColors);
SkinColor = SelectRandomColor(SkinColors);
}
private void CheckColors()
{
if (HairColor == Color.Black)
{
HairColor = HairColors.GetRandom();
HairColor = SelectRandomColor(HairColors);
}
if (FacialHairColor == Color.Black)
{
FacialHairColor = FacialHairColors.GetRandom();
FacialHairColor = SelectRandomColor(FacialHairColors);
}
if (SkinColor == Color.Black)
{
SkinColor = SkinColors.GetRandom();
SkinColor = SelectRandomColor(SkinColors);
}
}
@@ -610,7 +619,6 @@ namespace Barotrauma
idCounter++;
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
string genderStr = infoElement.GetAttributeString("gender", "male").ToLowerInvariant();
Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
UnlockedTalents = new HashSet<string>(infoElement.GetAttributeStringArray("unlockedtalents", new string[0], convertToLowerInvariant: true));
@@ -642,10 +650,10 @@ namespace Barotrauma
{
race = GetRandomRace(Rand.RandSync.Unsynced);
}
HairColors = CharacterConfigElement.GetAttributeColorArray("haircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeColorArray("facialhaircolors", new Color[] { Color.WhiteSmoke }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeColorArray("skincolors", new Color[] { new Color(255, 215, 200, 255) }).ToImmutableArray();
HairColors = CharacterConfigElement.GetAttributeTupleArray("haircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
FacialHairColors = CharacterConfigElement.GetAttributeTupleArray("facialhaircolors", new (Color, float)[] { (Color.WhiteSmoke, 100f) }).ToImmutableArray();
SkinColors = CharacterConfigElement.GetAttributeTupleArray("skincolors", new (Color, float)[] { (new Color(255, 215, 200, 255), 100f) }).ToImmutableArray();
RecreateHead(
infoElement.GetAttributeInt("headspriteid", 1),
race,
@@ -655,9 +663,35 @@ namespace Barotrauma
infoElement.GetAttributeInt("moustacheindex", -1),
infoElement.GetAttributeInt("faceattachmentindex", -1));
SkinColor = infoElement.GetAttributeColor("skincolor", Color.White);
HairColor = infoElement.GetAttributeColor("haircolor", Color.White);
FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.White);
//backwards compatibility
if (infoElement.Attribute("skincolor") == null && infoElement.Attribute("race") != null)
{
string raceStr = infoElement.GetAttributeString("race", string.Empty);
Race obsoleteRace = Race.None;
Enum.TryParse(raceStr, ignoreCase: true, out obsoleteRace);
switch (obsoleteRace)
{
case Race.White:
case Race.None:
SkinColor = new Color(255, 215, 200, 255);
break;
case Race.Brown:
SkinColor = new Color(158, 95, 72, 255);
break;
case Race.Black:
SkinColor = new Color(153, 75, 42, 255);
break;
case Race.Asian:
SkinColor = new Color(191, 116, 61, 255);
break;
}
}
else
{
SkinColor = infoElement.GetAttributeColor("skincolor", Color.Black);
}
HairColor = infoElement.GetAttributeColor("haircolor", Color.Black);
FacialHairColor = infoElement.GetAttributeColor("facialhaircolor", Color.Black);
CheckColors();
if (string.IsNullOrEmpty(Name))
@@ -1128,7 +1162,7 @@ namespace Barotrauma
return (int)(salary * Job.Prefab.PriceMultiplier);
}
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 pos)
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 pos, bool gainedFromApprenticeship = false)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
@@ -1148,7 +1182,7 @@ namespace Barotrauma
{
// assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilityValueStringCharacter(increaseSinceLastSkillPoint, skillIdentifier, Character);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromApprenticeship);
Character.CheckTalents(AbilityEffectType.OnGainSkillPoint, abilitySkillGain);
foreach (Character character in Character.GetFriendlyCrew(Character))
{
@@ -1747,4 +1781,19 @@ namespace Barotrauma
RemoveAfterRound = retainAfterRound;
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter
{
public AbilitySkillGain(float value, string abilityString, Character character, bool gainedFromApprenticeship)
{
Value = value;
String = abilityString;
Character = character;
GainedFromApprenticeship = gainedFromApprenticeship;
}
public Character Character { get; set; }
public float Value { get; set; }
public string String { get; set; }
public bool GainedFromApprenticeship { get; set; }
}
}
@@ -749,7 +749,7 @@ namespace Barotrauma
StunTimer = Stun > 0 ? StunTimer + deltaTime : 0;
FaceTint = DefaultFaceTint;
if (Character.GodMode) { return; }
for (int i = 0; i < limbHealths.Count; i++)
{
@@ -775,10 +775,6 @@ namespace Barotrauma
{
UpdateBleedingProjSpecific(bleeding, targetLimb, deltaTime);
}
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
}
@@ -798,10 +794,6 @@ namespace Barotrauma
var affliction = afflictions[i];
affliction.Update(this, null, deltaTime);
affliction.DamagePerSecondTimer += deltaTime;
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
Character.StackSpeedMultiplier(affliction.GetSpeedMultiplier());
}
@@ -818,7 +810,7 @@ namespace Barotrauma
}
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
@@ -827,6 +819,32 @@ namespace Barotrauma
}
}
private void UpdateSkinTint()
{
FaceTint = DefaultFaceTint;
BodyTint = Color.TransparentBlack;
for (int i = 0; i < limbHealths.Count; i++)
{
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
{
var affliction = limbHealths[i].Afflictions[j];
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
}
}
for (int i = 0; i < afflictions.Count; i++)
{
var affliction = afflictions[i];
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
}
}
private void UpdateOxygen(float deltaTime)
{
if (!Character.NeedsOxygen) { return; }
@@ -905,6 +923,7 @@ namespace Barotrauma
if (Unkillable || Character.GodMode) { return; }
var (type, affliction) = GetCauseOfDeath();
UpdateSkinTint();
Character.Kill(type, affliction);
#if CLIENT
DisplayVitalityDelay = 0.0f;
@@ -218,7 +218,7 @@ namespace Barotrauma
public Vector2 StepOffset => ConvertUnits.ToSimUnits(Params.StepOffset) * ragdoll.RagdollParams.JointScale;
public bool inWater;
public bool InWater { get; set; }
private FixedMouseJoint pullJoint;
@@ -535,8 +535,11 @@ namespace Barotrauma
public string Name => Params.Name;
// Exposed for status effects
// These properties are exposed for status effects
public bool IsDead => character.IsDead;
public float Health => character.Health;
public float HealthPercentage => character.HealthPercentage;
public AIState AIState => character.AIController is EnemyAIController enemyAI ? enemyAI.State : AIState.Idle;
public bool CanBeSeveredAlive
{
@@ -804,7 +807,7 @@ namespace Barotrauma
{
UpdateProjSpecific(deltaTime);
if (inWater)
if (InWater)
{
body.ApplyWaterForces();
}
@@ -847,20 +850,25 @@ namespace Barotrauma
attack?.UpdateCoolDown(deltaTime);
}
private bool temporarilyDisabled;
private float reEnableTimer = -1;
public void HideAndDisable(float duration = 0)
public void HideAndDisable(float duration = 0, bool ignoreCollisions = true)
{
if (Hidden || Disabled) { return; }
if (ignoreCollisions && IgnoreCollisions) { return; }
temporarilyDisabled = true;
Hidden = true;
Disabled = true;
IgnoreCollisions = true;
IgnoreCollisions = ignoreCollisions;
if (duration > 0)
{
reEnableTimer = duration;
}
}
private void ReEnable()
public void ReEnable()
{
if (!temporarilyDisabled) { return; }
Hidden = false;
Disabled = false;
IgnoreCollisions = false;
@@ -14,9 +14,9 @@ namespace Barotrauma
NotDefined,
Walk,
Run,
Crouch,
SwimSlow,
SwimFast
SwimFast,
Crouch
}
abstract class GroundedMovementParams : AnimationParams
@@ -79,12 +79,18 @@ namespace Barotrauma
[Serialize(10f, true, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
[Serialize(true, true), Editable]
public bool UsePathFinding { get; set; }
[Serialize(1f, true, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)]
public float PathFinderPriority { get; set; }
[Serialize(false, true), Editable]
public bool HideInSonar { get; set; }
[Serialize(false, true), Editable]
public bool HideInThermalGoggles { get; set; }
[Serialize(0f, true), Editable]
public float SonarDisruption { get; set; }
@@ -706,6 +712,9 @@ namespace Barotrauma
[Serialize(-1f, true, description: "A generic max threshold. Not used if set to negative."), Editable]
public float ThresholdMax { get; private set; }
[Serialize("0.0, 0.0", true), Editable]
public Vector2 Offset { get; private set; }
[Serialize(AttackPattern.Straight, true), Editable]
public AttackPattern AttackPattern { get; set; }
@@ -597,6 +597,9 @@ namespace Barotrauma
[Serialize(float.NaN, true, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
public float SpriteOrientation { get; set; }
[Serialize(LimbType.None, true, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")]
public LimbType InheritLimbDepth { get; set; }
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
public float SteerForce { get; set; }
@@ -679,6 +682,9 @@ namespace Barotrauma
[Serialize(50f, true), Editable]
public float BlinkForce { get; set; }
[Serialize(false, true), Editable]
public bool OnlyBlinkInWater { get; set; }
[Serialize(TransitionMode.Linear, true), Editable]
public TransitionMode BlinkTransitionIn { get; private set; }
@@ -87,19 +87,6 @@ namespace Barotrauma.Abilities
public string String { get; set; }
}
class AbilityValueStringCharacter : AbilityObject, IAbilityValue, IAbilityString, IAbilityCharacter
{
public AbilityValueStringCharacter(float value, string abilityString, Character character)
{
Value = value;
String = abilityString;
Character = character;
}
public Character Character { get; set; }
public float Value { get; set; }
public string String { get; set; }
}
class AbilityStringCharacter : AbilityObject, IAbilityCharacter, IAbilityString
{
public AbilityStringCharacter(string abilityString, Character character)
@@ -125,7 +125,6 @@ namespace Barotrauma.Abilities
return null;
}
DebugConsole.AddWarning("Instantiated " + characterAbility + " for talent " + characterAbilityGroup.CharacterTalent.DebugIdentifier);
return characterAbility;
}
}
@@ -1,33 +1,60 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyForce : CharacterAbility
{
private readonly float impulseStrength;
private readonly float force;
private readonly float maxVelocity;
private readonly string afflictionIdentifier;
private readonly HashSet<LimbType> limbTypes = new HashSet<LimbType>();
public override bool AppliesEffectOnIntervalUpdate => true;
public CharacterAbilityApplyForce(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
impulseStrength = abilityElement.GetAttributeFloat("impulsestrength", 0f);
force = abilityElement.GetAttributeFloat("force", 0f);
maxVelocity = abilityElement.GetAttributeFloat("maxvelocity", 10f);
afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", "");
string[] limbTypesStr = abilityElement.GetAttributeStringArray("limbtypes", new string[0]);
foreach (string limbTypeStr in limbTypesStr)
{
if (Enum.TryParse(limbTypeStr, out LimbType limbType))
{
limbTypes.Add(limbType);
}
else
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.");
}
}
}
protected override void ApplyEffect()
{
Affliction affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier);
if (affliction == null) { return; }
float strength = 1.0f;
if (!string.IsNullOrEmpty(afflictionIdentifier))
{
Affliction affliction = Character.CharacterHealth.GetAffliction(afflictionIdentifier);
if (affliction == null) { return; }
strength = affliction.Strength / affliction.Prefab.MaxStrength;
}
foreach (Limb limb in Character.AnimController.Limbs)
{
limb.body.ApplyForce(Vector2.Normalize(limb.Mass * Character.AnimController.TargetMovement) * impulseStrength * (affliction.Strength / affliction.Prefab.MaxStrength), maxVelocity);
if (limb.IsSevered || limb.Removed) { continue; }
if (limbTypes.Any())
{
if (!limbTypes.Contains(limb.type)) { continue; }
}
if (Character.AnimController.TargetMovement.LengthSquared() < 0.001f) { continue; }
limb.body.ApplyForce(Vector2.Normalize(limb.Mass * Character.AnimController.TargetMovement) * force * strength, maxVelocity);
}
}
}
@@ -8,13 +8,13 @@ namespace Barotrauma.Abilities
class CharacterAbilityAlienHoarder : CharacterAbility
{
private readonly float addedDamageMultiplierPerItem;
private readonly int maxAmount;
private readonly float maxAddedDamageMultiplier;
private readonly string[] tags;
public CharacterAbilityAlienHoarder(CharacterAbilityGroup characterAbilityGroup, XElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
addedDamageMultiplierPerItem = abilityElement.GetAttributeFloat("addeddamagemultiplierperitem", 0f);
maxAmount = abilityElement.GetAttributeInt("maxamount", 0);
maxAddedDamageMultiplier = abilityElement.GetAttributeFloat("maxaddedddamagemultiplier", float.MaxValue);
tags = abilityElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
}
@@ -30,7 +30,7 @@ namespace Barotrauma.Abilities
totalAddedDamageMultiplier += addedDamageMultiplierPerItem;
}
}
attackData.DamageMultiplier += addedDamageMultiplierPerItem;
attackData.DamageMultiplier += Math.Min(totalAddedDamageMultiplier, maxAddedDamageMultiplier);
}
else
{
@@ -12,9 +12,9 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityString)?.String is string skillIdentifier && (abilityObject as IAbilityCharacter)?.Character is Character character)
if (abilityObject is AbilitySkillGain abilitySkillGain && !abilitySkillGain.GainedFromApprenticeship && abilitySkillGain.Character != Character)
{
Character.Info?.IncreaseSkillLevel(skillIdentifier, 1.0f, character.Position + Vector2.UnitY * 175.0f);
Character.Info?.IncreaseSkillLevel(abilitySkillGain.String, 1.0f, Character.Position + Vector2.UnitY * 175.0f, gainedFromApprenticeship: true);
}
}
}
@@ -58,12 +58,7 @@ namespace Barotrauma
return handle;
}
public static CoroutineHandle Invoke(Action action)
{
return StartCoroutine(DoInvokeAfter(action, 0.0f));
}
public static CoroutineHandle InvokeAfter(Action action, float delay)
public static CoroutineHandle Invoke(Action action, float delay = 0f)
{
return StartCoroutine(DoInvokeAfter(action, delay));
}
@@ -845,15 +845,24 @@ namespace Barotrauma
var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled;
if (character != null)
{
character.GiveTalent(args[0]);
TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c =>
c.Identifier.Equals(args[0], StringComparison.OrdinalIgnoreCase) ||
c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase));
if (talentPrefab == null)
{
ThrowError($"Couldn't find the talent \"{args[0]}\".");
return;
}
character.GiveTalent(talentPrefab);
NewMessage($"Gave talent \"{talentPrefab.DisplayName}\" to \"{character.Name}\".");
}
},
() =>
{
List<string> talentNames = new List<string>();
foreach (TalentPrefab itemPrefab in TalentPrefab.TalentPrefabs)
foreach (TalentPrefab talent in TalentPrefab.TalentPrefabs)
{
talentNames.Add(itemPrefab.Identifier);
talentNames.Add(talent.DisplayName);
}
return new string[][]
@@ -863,24 +872,15 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]]: give the controlled characters all the talents of the specified class", (string[] args) =>
commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]] [character]: give the specified character all the talents of the specified class", (string[] args) =>
{
if (Character.Controlled == null) { return; }
if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
foreach (var talentTree in TalentTree.JobTalentTrees)
{
foreach (var subTree in talentTree.Value.TalentSubTrees)
{
foreach (var option in subTree.TalentOptionStages)
{
foreach (var talent in option.Talents)
{
Character.Controlled.GiveTalent(talent);
}
}
}
}
var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled;
if (character == null) { return; }
List<TalentTree> talentTrees = new List<TalentTree>();
if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
{
talentTrees.AddRange(TalentTree.JobTalentTrees.Values);
}
else
{
@@ -893,15 +893,21 @@ namespace Barotrauma
if (!TalentTree.JobTalentTrees.TryGetValue(job.Identifier, out TalentTree talentTree))
{
ThrowError($"No talents configured for the job \"{args[0]}\".");
return;
return;
}
talentTrees.Add(talentTree);
}
foreach (var talentTree in talentTrees)
{
foreach (var subTree in talentTree.TalentSubTrees)
{
foreach (var option in subTree.TalentOptionStages)
{
foreach (var talent in option.Talents)
{
Character.Controlled.GiveTalent(talent);
character.GiveTalent(talent);
NewMessage($"Unlocked talent \"{talent.DisplayName}\".");
}
}
}
@@ -913,7 +919,8 @@ namespace Barotrauma
availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name));
return new string[][]
{
availableArgs.ToArray()
availableArgs.ToArray(),
Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
};
}, isCheat: true));
@@ -1,6 +1,4 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class GodModeAction : EventAction
{
[Serialize(true, true)]
public bool Enabled { get; set; }
[Serialize("", true)]
public string TargetTag { get; set; }
public GodModeAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private bool isFinished = false;
public override bool IsFinished(ref string goTo)
{
return isFinished;
}
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag);
foreach (var target in targets)
{
if (target != null && target is Character character)
{
character.GodMode = Enabled;
}
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(GodModeAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
(Enabled ? "Enable godmode" : "Disable godmode");
}
}
}
@@ -91,7 +91,7 @@ namespace Barotrauma
int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1);
for (int i = 0; i < amount; i++)
{
CoroutineManager.InvokeAfter(() =>
CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
@@ -359,10 +359,19 @@ namespace Barotrauma
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnAllyGainMissionExperience, experienceGainMultiplier));
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
int experienceGain = (int)(baseExperienceGain * experienceGainMultiplier.Value);
#if CLIENT
foreach (Character character in crewCharacters)
{
character.Info.GiveExperience((int)(baseExperienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
character.Info.GiveExperience(experienceGain, isMissionExperience: true);
}
#else
foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients)
{
//give the experience to the stored characterinfo if the client isn't currently controlling a character
(c.Character?.Info ?? c.CharacterInfo)?.GiveExperience(experienceGain, isMissionExperience: true);
}
#endif
// apply money gains afterwards to prevent them from affecting XP gains
var moneyGainMission = new AbilityValueMission(1f, this);
@@ -21,10 +21,11 @@ namespace Barotrauma
private bool disallowed;
private readonly Level.PositionType spawnPosType;
private readonly string spawnPointTag;
private bool spawnPending;
private int maxAmountPerLevel = int.MaxValue;
private readonly int maxAmountPerLevel = int.MaxValue;
public List<Character> Monsters => monsters;
public Vector2? SpawnPos => spawnPos;
@@ -87,6 +88,8 @@ namespace Barotrauma
spawnPosType = Level.PositionType.Abyss;
}
spawnPointTag = prefab.ConfigElement.GetAttributeString("spawnpointtag", string.Empty);
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
@@ -285,18 +288,19 @@ namespace Barotrauma
spawnPos = chosenPosition.Position.ToVector2();
if (chosenPosition.Submarine != null || chosenPosition.Ruin != null)
{
var spawnPoint = WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false);
if (spawnPoint != null)
var spawnPoint =
WayPoint.GetRandom(SpawnType.Enemy, sub: chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine, useSyncedRand: false, spawnPointTag: spawnPointTag);
if (spawnPoint != null)
{
System.Diagnostics.Debug.Assert(spawnPoint.Submarine == (chosenPosition.Submarine ?? chosenPosition.Ruin?.Submarine));
spawnPos = spawnPoint.WorldPosition;
spawnPos = spawnPoint.WorldPosition;
}
else
{
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
return;
return;
}
}
else if ((chosenPosition.PositionType == Level.PositionType.MainPath || chosenPosition.PositionType == Level.PositionType.SidePath)
@@ -447,7 +451,7 @@ namespace Barotrauma
for (int i = 0; i < amount; i++)
{
string seed = Level.Loaded.Seed + i.ToString();
CoroutineManager.InvokeAfter(() =>
CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
@@ -10,10 +10,15 @@ namespace Barotrauma
{
public static bool OutputDebugInfo = false;
/// <summary>
/// If we are spawning in an area where difficulty should not be a factor, assume difficulty is at the exact "middle"
/// </summary>
public const float DefaultDifficultyModifier = 0f;
public static void PlaceIfNeeded()
{
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; }
@@ -23,6 +28,7 @@ namespace Barotrauma
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
}
float difficultyModifier = GetLevelDifficultyModifier();
foreach (var sub in Submarine.Loaded)
{
if (sub.Info.Type == SubmarineType.Player ||
@@ -32,7 +38,7 @@ namespace Barotrauma
{
continue;
}
Place(sub.ToEnumerable());
Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier);
}
if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost)
@@ -42,12 +48,23 @@ namespace Barotrauma
}
}
public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer)
private const float MaxDifficultyModifier = 0.2f;
/// <summary>
/// Spawn probability of loot is modified by difficulty, -20% less loot at 0% difficulty and +20% loot at 100% difficulty.
/// </summary>
private static float GetLevelDifficultyModifier()
{
Place(sub.ToEnumerable(), regeneratedContainer);
return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier);
}
private static void Place(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null)
public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer)
{
// Level difficulty currently doesn't affect regenerated loot for the sake of simplicity
Place(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
}
private static void Place(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null, float difficultyModifier = DefaultDifficultyModifier)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
@@ -167,7 +184,7 @@ namespace Barotrauma
}
foreach (var validContainer in validContainers)
{
var newItems = SpawnItem(itemPrefab, containers, validContainer);
var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier);
if (newItems.Any())
{
spawnedItems.AddRange(newItems);
@@ -201,11 +218,18 @@ namespace Barotrauma
return validContainers;
}
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
private static readonly float[] qualityCommonnesses = new float[Quality.MaxQuality + 1]
{
0.85f,
0.125f,
0.0225f,
0.0025f,
};
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer, float difficultyModifier)
{
List<Item> spawnedItems = new List<Item>();
bool success = false;
if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability) { return spawnedItems; }
if (Rand.Value(Rand.RandSync.Server) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; }
// Don't add dangerously reactive materials in thalamus wrecks
if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater"))
{
@@ -220,10 +244,24 @@ namespace Barotrauma
break;
}
if (!validContainer.Key.Inventory.CanBePut(itemPrefab)) { break; }
int quality = 0;
float qualityCommmonnessSum = qualityCommonnesses.Sum();
float randomNumber = Rand.Range(0f, qualityCommmonnessSum, Rand.RandSync.Server);
for (int k = qualityCommonnesses.Length - 1; k >= 0; k--)
{
if (randomNumber < qualityCommonnesses[k])
{
quality = k;
break;
}
}
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine)
{
SpawnedInOutpost = validContainer.Key.Item.SpawnedInOutpost,
AllowStealing = validContainer.Key.Item.AllowStealing,
Quality = quality,
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
OriginalContainerIndex =
Item.ItemList.Where(it => it.Submarine == validContainer.Key.Item.Submarine && it.OriginalModuleIndex == validContainer.Key.Item.OriginalModuleIndex).ToList().IndexOf(validContainer.Key.Item)
@@ -235,7 +273,6 @@ namespace Barotrauma
spawnedItems.Add(item);
validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false);
containers.AddRange(item.GetComponents<ItemContainer>());
success = true;
}
return spawnedItems;
}
@@ -563,13 +563,16 @@ namespace Barotrauma
public override void End(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None)
{
List<Item> takenItems = new List<Item>();
foreach (Item item in Item.ItemList)
if (Level.Loaded?.Type == LevelData.LevelType.Outpost)
{
if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; }
var owner = item.GetRootInventoryOwner();
if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
foreach (Item item in Item.ItemList)
{
takenItems.Add(item);
if (!item.SpawnedInOutpost || item.OriginalModuleIndex < 0) { continue; }
var owner = item.GetRootInventoryOwner();
if ((!(owner?.Submarine?.Info?.IsOutpost ?? false)) || (owner is Character character && character.TeamID == CharacterTeamType.Team1) || item.Submarine == null || !item.Submarine.Info.IsOutpost)
{
takenItems.Add(item);
}
}
}
if (map != null && CargoManager != null)
@@ -0,0 +1,280 @@
#nullable enable
using System;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
internal partial class EntitySpawnerComponent : ItemComponent, IDrawableComponent
{
public enum AreaShape
{
Rectangle,
Circle
}
[Editable, Serialize("", true, "Identifier of the item to spawn, does nothing if SpeciesName is set. Separate by comma to have multiple items spawn at random.")]
public string? ItemIdentifier { get; set; }
[Editable, Serialize("", true, "Species name of the creature to spawn, takes priority if ItemIdentifier is set. Separate by comma to have multiple creatures spawn at random.")]
public string? SpeciesName { get; set; }
[Editable, Serialize(true, true, "Only spawn if crew members are within certain area")]
public bool OnlySpawnWhenCrewInRange { get; set; }
[Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where crew members need to stay")]
public AreaShape CrewAreaShape { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where crew members need to stay. Does nothing if CrewAreaShape is set to Circle")]
public Vector2 CrewAreaBounds { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle to spawn stuff in. Does nothing if CrewAreaShape is set to Rectangle")]
public float CrewAreaRadius { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the crew area from the center of the item")]
public Vector2 CrewAreaOffset { get; set; }
[Editable, Serialize(AreaShape.Rectangle, true, "Shape of the area where enemies or items are spawned")]
public AreaShape SpawnAreaShape { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize("500,500", true, "Size of the rectangle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Circle")]
public Vector2 SpawnAreaBounds { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 0, ValueStep = 10f), Serialize(500f, true, "Radius of the circle where items or creatures will be spawned. Does nothing if SpawnAreaShape is set to Rectangle")]
public float SpawnAreaRadius { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize("0,0", true, "Offset of the spawn area from the center of the item")]
public Vector2 SpawnAreaOffset { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 1f), Serialize("10,40", true, "Time range between spawn attempts in seconds")]
public Vector2 SpawnTimerRange { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = 1f, ValueStep = 1f, DecimalCount = 0), Serialize("1,3", true, "Minumum and maximum amount of items or creatures to spawn in one attempt")]
public Vector2 SpawnAmountRange { get; set; }
[Editable(MinValueInt = 0), Serialize(8, true, "Amount of items or creatures in the spawn area that will prevent further items or creatures from being spawned")]
public int MaximumAmount { get; set; }
[Editable(MaxValueFloat = int.MaxValue, MinValueFloat = int.MinValue, ValueStep = 10f), Serialize(500f, true, "Inflate the circle of rectangle by this value to extend the area that counts towards the maximum amount of items or enemies to be spawned")]
public float MaximumAmountRangePadding { get; set; }
[Serialize(true, true, "")]
public bool CanSpawn { get; set; } = true;
private float SpawnTimer;
private float? SpawnTimerGoal;
public EntitySpawnerComponent(Item item, XElement element) : base(item, element)
{
IsActive = true;
}
public override void OnItemLoaded()
{
if (!string.IsNullOrWhiteSpace(ItemIdentifier))
{
string[] allItems = ItemIdentifier.Split(',');
foreach (string itemIdentifier in allItems)
{
string trimmedString = itemIdentifier.Trim();
bool found = false;
foreach (ItemPrefab prefab in ItemPrefab.Prefabs)
{
if (string.Equals(trimmedString, prefab.Identifier, StringComparison.OrdinalIgnoreCase))
{
found = true;
break;
}
}
if (!found)
{
DebugConsole.ThrowError($"Error loading {nameof(EntitySpawnerComponent)} - item prefab \"" + name + "\" (identifier \"" + trimmedString + "\") not found.");
}
}
}
base.OnItemLoaded();
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
item.SendSignal(CanSpawn ? "1" : "0", "state_out");
if (GameMain.NetworkMember is { IsClient: true }) { return; }
SpawnTimerGoal ??= Rand.Range(Math.Min(SpawnTimerRange.X, SpawnTimerRange.Y), Math.Max(SpawnTimerRange.X, SpawnTimerRange.Y), Rand.RandSync.Unsynced);
SpawnTimer += deltaTime;
if (SpawnTimer > SpawnTimerGoal)
{
Spawn();
SpawnTimerGoal = null;
SpawnTimer = 0;
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
bool isNonZero = signal.value != "0";
switch (connection.Name)
{
case "set_state":
CanSpawn = isNonZero;
break;
case "toggle" when isNonZero:
CanSpawn = !CanSpawn;
break;
}
}
private RectangleF GetAreaRectangle(Vector2 size, Vector2 offset, bool draw)
{
Vector2 pos = item.WorldPosition;
if (draw)
{
pos.Y = -pos.Y;
}
pos += offset;
RectangleF rect = new RectangleF(pos.X - size.X / 2f, pos.Y - size.Y / 2f, size.X, size.Y);
return rect;
}
private bool CanSpawnMore()
{
if (!CanSpawn) { return false; }
if (OnlySpawnWhenCrewInRange)
{
if (!Character.CharacterList.Any(c => !c.IsDead && c.IsOnPlayerTeam && IsInRange(c.WorldPosition, crewArea: true, rangePad: false)))
{
return false;
}
}
int amount;
if (!string.IsNullOrWhiteSpace(SpeciesName))
{
amount = Character.CharacterList.Count(c => !c.IsDead && c.SpeciesName.Equals(SpeciesName, StringComparison.OrdinalIgnoreCase) && IsInRange(c.WorldPosition, crewArea: false, rangePad: true));
}
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
{
amount = Item.ItemList.Count(it => it.Submarine == item.Submarine && it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.OrdinalIgnoreCase) && IsInRange(it.WorldPosition, crewArea: false, rangePad: true));
}
else
{
return false;
}
return amount < MaximumAmount;
}
private bool IsInRange(Vector2 worldPos, bool crewArea = false, bool rangePad = false)
{
Vector2 offset = crewArea ? CrewAreaOffset : SpawnAreaOffset;
offset.Y = -offset.Y;
switch (crewArea ? CrewAreaShape : SpawnAreaShape)
{
case AreaShape.Circle:
Vector2 center = item.WorldPosition + offset;
float distance = (crewArea ? CrewAreaRadius : SpawnAreaRadius) + (rangePad ? MaximumAmountRangePadding : 0);
return Vector2.DistanceSquared(worldPos, center) < distance * distance;
case AreaShape.Rectangle:
RectangleF rect = GetAreaRectangle(crewArea ? CrewAreaBounds : SpawnAreaBounds, offset, draw: false);
if (rangePad)
{
rect.Inflate(MaximumAmountRangePadding, MaximumAmountRangePadding);
}
return rect.Contains(worldPos);
}
return false;
}
public void Spawn()
{
if (!CanSpawnMore()) { return; }
int minAmount = Math.Min((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y),
maxAmount = Math.Max((int)SpawnAmountRange.X, (int)SpawnAmountRange.Y);
int amount = Rand.Range(minAmount, maxAmount, Rand.RandSync.Unsynced);
Vector2 offset = SpawnAreaOffset;
offset.Y = -offset.Y;
switch (SpawnAreaShape)
{
case AreaShape.Circle:
{
var (x, y) = item.WorldPosition + offset;
for (int i = 0; i < Math.Max(1, amount); i++)
{
float angle = Rand.Range(-MathHelper.TwoPi, MathHelper.TwoPi);
float distance = Rand.Range(0, SpawnAreaRadius, Rand.RandSync.Unsynced);
Vector2 spawnPos = new Vector2(x + distance * (float)Math.Cos(angle), y + distance * (float)Math.Sin(angle));
SpawnEntity(spawnPos);
}
break;
}
case AreaShape.Rectangle:
{
RectangleF rect = GetAreaRectangle(SpawnAreaBounds, offset, draw: false);
for (int i = 0; i < Math.Max(1, amount); i++)
{
float minX = Math.Min(rect.Left, rect.Right),
maxX = Math.Max(rect.Left, rect.Right),
minY = Math.Min(rect.Top, rect.Bottom),
maxY = Math.Max(rect.Top, rect.Bottom);
Vector2 spawnPos = new Vector2(Rand.Range(minX, maxX, Rand.RandSync.Unsynced), Rand.Range(minY, maxY, Rand.RandSync.Unsynced));
SpawnEntity(spawnPos);
}
break;
}
}
void SpawnEntity(Vector2 pos)
{
if (!string.IsNullOrWhiteSpace(SpeciesName))
{
string[] allSpecies = SpeciesName.Split(',');
string species = allSpecies.GetRandom().Trim();
Entity.Spawner.AddToSpawnQueue(species, pos);
}
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
{
string[] allItems = ItemIdentifier.Split(',');
string itemIdentifier = allItems.GetRandom().Trim();
ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier);
if (prefab is null) { return; }
if (item.Submarine is { } sub)
{
pos -= sub.Position;
}
Entity.Spawner.AddToSpawnQueue(prefab, pos, item.Submarine);
}
}
}
}
}
@@ -598,9 +598,12 @@ namespace Barotrauma.Items.Components
DebugConsole.AddWarning("Character without CharacterInfo attempting to attach a limited attachable item!");
return false;
}
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
Structure attachTarget = Structure.GetAttachTarget(attachPos);
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
int currentlyAttachedCount = Item.ItemList.Count(
i => i.Submarine == item.Submarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier);
i => i.Submarine == attachTarget?.Submarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.prefab.Identifier);
if (currentlyAttachedCount >= maxAttachableCount)
{
#if CLIENT
@@ -77,6 +77,7 @@ namespace Barotrauma.Items.Components
};
}
item.IsShootable = true;
item.RequireAimToUse = element.Parent.GetAttributeBool("requireaimtouse", true);
PreferredContainedItems = element.GetAttributeStringArray("preferredcontaineditems", new string[0], convertToLowerInvariant: true);
}
@@ -210,7 +211,7 @@ namespace Barotrauma.Items.Components
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
if (aim)
{
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 5f, MathHelper.PiOver4));
hitPos = MathUtils.WrapAnglePi(Math.Min(hitPos + deltaTime * 3f, MathHelper.PiOver4));
ac.HoldItem(deltaTime, item, handlePos, aimPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos, aimMelee: true);
}
else
@@ -222,16 +223,16 @@ namespace Barotrauma.Items.Components
else
{
// TODO: We might want to make this configurable
hitPos = MathUtils.WrapAnglePi(hitPos - deltaTime * 15f);
hitPos -= deltaTime * 15f;
if (Swing)
{
ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle + hitPos);
ac.HoldItem(deltaTime, item, handlePos, SwingPos, Vector2.Zero, aim: false, hitPos, holdAngle);
}
else
{
ac.HoldItem(deltaTime, item, handlePos, holdPos, Vector2.Zero, aim: false, holdAngle);
}
if (hitPos < -MathHelper.PiOver2)
if (hitPos < -MathHelper.Pi)
{
RestoreCollision();
hitting = false;
@@ -74,8 +74,8 @@ namespace Barotrauma.Items.Components
{
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
Vector2 flippedPos = barrelPos;
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
return Vector2.Transform(flippedPos, bodyTransform);
if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
return Vector2.Transform(flippedPos, bodyTransform) * item.Scale;
}
}
@@ -712,7 +712,7 @@ namespace Barotrauma.Items.Components
humanAnim.Crouching = true;
}
}
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.inWater))
if (dist > reach * 0.8f || dist > reach * 0.5f && character.AnimController.Limbs.Any(l => l.InWater))
{
// Steer closer
if (character.AIController.SteeringManager is IndoorsSteeringManager indoorSteering)
@@ -522,7 +522,7 @@ namespace Barotrauma.Items.Components
if (Math.Abs(item.Rotation) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(MathHelper.ToRadians(-item.Rotation));
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position;
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
@@ -550,6 +550,10 @@ namespace Barotrauma.Items.Components
currentRotation *= item.body.Dir;
currentRotation += item.body.Rotation;
}
else
{
currentRotation += MathHelper.ToRadians(-item.Rotation);
}
int i = 0;
Vector2 currentItemPos = transformedItemPos;
@@ -64,13 +64,6 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize(true, true, description: "Enable hull condition mode.")]
public bool EnableHullCondition
{
get;
set;
}
[Editable, Serialize(true, true, description: "Enable item finder mode.")]
public bool EnableItemFinder
{
@@ -148,30 +141,49 @@ namespace Barotrauma.Items.Components
hullDatas.Add(sourceHull, hullData);
}
if (hullData.Distort) return;
if (hullData.Distort) { return; }
switch (connection.Name)
{
case "water_data_in":
//cheating a bit because water detectors don't actually send the water level
float waterAmount;
if (source.GetComponent<WaterDetector>() == null)
{
hullData.ReceivedWaterAmount = Rand.Range(0.0f, 1.0f);
waterAmount = Rand.Range(0.0f, 1.0f);
}
else
{
hullData.ReceivedWaterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
waterAmount = Math.Min(sourceHull.WaterVolume / sourceHull.Volume, 1.0f);
}
hullData.ReceivedWaterAmount = waterAmount;
foreach (var linked in sourceHull.linkedTo)
{
if (!(linked is Hull linkedHull)) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedWaterAmount = waterAmount;
}
break;
case "oxygen_data_in":
float oxy;
if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out oxy))
if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy))
{
oxy = Rand.Range(0.0f, 100.0f);
}
hullData.ReceivedOxygenAmount = oxy;
foreach (var linked in sourceHull.linkedTo)
{
if (!(linked is Hull linkedHull)) { continue; }
if (!hullDatas.TryGetValue(linkedHull, out HullData linkedHullData))
{
linkedHullData = new HullData();
hullDatas.Add(linkedHull, linkedHullData);
}
linkedHullData.ReceivedOxygenAmount = oxy;
}
break;
}
}
@@ -9,6 +9,14 @@ namespace Barotrauma.Items.Components
{
public const int MaxQuality = 3;
public static readonly float[] QualityCommonnesses = new float[]
{
0.8f,
0.15f,
0.045f,
0.005f,
};
public enum StatType
{
Condition,
@@ -39,7 +47,18 @@ namespace Barotrauma.Items.Components
public int QualityLevel
{
get { return qualityLevel; }
set { qualityLevel = MathHelper.Clamp(value, 0, MaxQuality); }
set
{
if (value == qualityLevel) { return; }
bool wasInFullCondition = item.IsFullCondition;
qualityLevel = MathHelper.Clamp(value, 0, MaxQuality);
//set the condition to the new max condition
if (wasInFullCondition && statValues.ContainsKey(StatType.Condition))
{
item.Condition = item.MaxCondition;
}
}
}
public Quality(Item item, XElement element) : base(item, element)
@@ -376,7 +376,7 @@ namespace Barotrauma.Items.Components
tinkeringDuration -= deltaTime;
// not great to interject it here, should be less reliant on returning
float conditionDecrease = deltaTime * (CurrentFixer.GetStatValue(StatTypes.TinkeringDamage) / item.MaxCondition) * 100f;
float conditionDecrease = deltaTime * (CurrentFixer.GetStatValue(StatTypes.TinkeringDamage) / item.Prefab.Health) * 100f;
item.Condition -= conditionDecrease;
if (!CanTinker(CurrentFixer) || tinkeringDuration <= 0f)
@@ -424,7 +424,8 @@ namespace Barotrauma.Items.Components
}
else
{
float conditionIncrease = deltaTime / (fixDuration / item.MaxCondition);
// scale with prefab's health instead of real health to ensure repair speed remains static with upgrades
float conditionIncrease = deltaTime / (fixDuration / item.Prefab.Health);
item.Condition += conditionIncrease;
#if SERVER
GameMain.Server.KarmaManager.OnItemRepaired(CurrentFixer, this, conditionIncrease);
@@ -458,7 +459,8 @@ namespace Barotrauma.Items.Components
}
else
{
float conditionDecrease = deltaTime / (fixDuration / item.MaxCondition);
// scale with prefab's health instead of real health to ensure sabotage speed remains static with (any) upgrades
float conditionDecrease = deltaTime / (fixDuration / item.Prefab.Health);
item.Condition -= conditionDecrease;
}
@@ -159,7 +159,10 @@ namespace Barotrauma.Items.Components
{
lightColor = value;
#if CLIENT
if (Light != null) Light.Color = IsActive ? lightColor : Color.Transparent;
if (Light != null)
{
Light.Color = IsActive ? lightColor : Color.Transparent;
}
#endif
}
}
@@ -178,7 +178,7 @@ namespace Barotrauma.Items.Components
//signal strength diminishes by distance
float sentSignalStrength = signal.strength *
MathHelper.Clamp(1.0f - (Vector2.Distance(item.WorldPosition, wifiComp.item.WorldPosition) / wifiComp.range), 0.0f, 1.0f);
Signal s = new Signal(signal.value, ++signal.stepsTaken, sender: signal.sender, source: signal.source,
Signal s = new Signal(signal.value, signal.stepsTaken + 1, sender: signal.sender, source: signal.source,
power: 0.0f, strength: sentSignalStrength);
if (wifiComp.signalOutConnection != null)
@@ -48,8 +48,8 @@ namespace Barotrauma
return false;
}
}
if (items[0].Prefab.Identifier != item.Prefab.Identifier ||
items.Count + 1 > item.Prefab.MaxStackSize)
if (items[0].Quality != item.Quality) { return false; }
if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.MaxStackSize)
{
return false;
}
@@ -106,7 +106,7 @@ namespace Barotrauma
//a dictionary containing lists of the status effects in all the components of the item
private readonly bool[] hasStatusEffectsOfType;
private readonly Dictionary<ActionType, List<StatusEffect>> statusEffectLists;
public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }
private bool? hasInGameEditableProperties;
@@ -232,7 +232,7 @@ namespace Barotrauma
public bool IsInteractable(Character character)
{
if (character != null && character.IsOnPlayerTeam)
{
{
return IsPlayerTeamInteractable;
}
else
@@ -254,6 +254,12 @@ namespace Barotrauma
{
if (!Prefab.AllowRotatingInEditor) { return; }
rotationRad = MathHelper.ToRadians(value);
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen)
{
SetContainedItemPositions();
}
#endif
}
}
@@ -478,7 +484,7 @@ namespace Barotrauma
get => maxRepairConditionMultiplier;
set { maxRepairConditionMultiplier = MathHelper.Clamp(value, 0.0f, float.PositiveInfinity); }
}
//the default value should be Prefab.Health, but because we can't use it in the attribute,
//we'll just use NaN (which does nothing) and set the default value in the constructor/load
[Serialize(float.NaN, false), Editable]
@@ -628,9 +634,9 @@ namespace Barotrauma
public int Quality
{
get
{
return qualityComponent?.QualityLevel ?? 0;
get
{
return qualityComponent?.QualityLevel ?? 0;
}
set
{
@@ -1155,7 +1161,7 @@ namespace Barotrauma
{
return GetComponent<Quality>()?.GetValue(statType) ?? 0.0f;
}
public void RemoveContained(Item contained)
{
ownInventory?.RemoveItem(contained);
@@ -1867,7 +1873,8 @@ namespace Barotrauma
foreach (ItemComponent component in components)
{
component.FlipX(relativeToSub);
}
}
SetContainedItemPositions();
}
public override void FlipY(bool relativeToSub)
@@ -1892,6 +1899,7 @@ namespace Barotrauma
{
component.FlipY(relativeToSub);
}
SetContainedItemPositions();
}
/// <summary>
@@ -2112,8 +2120,6 @@ namespace Barotrauma
} while (CoroutineManager.DeltaTime <= 0.0f);
delayedSignals.Remove((signal, connection));
signal.source = this;
connection.SendSignal(signal);
yield return CoroutineStatus.Success;
@@ -2470,6 +2476,8 @@ namespace Barotrauma
parentInventory.RemoveItem(this);
parentInventory = null;
}
SetContainedItemPositions();
}
public void Equip(Character character)
@@ -2741,7 +2749,7 @@ namespace Barotrauma
{
CoroutineManager.StopCoroutines(logPropertyChangeCoroutine);
}
logPropertyChangeCoroutine = CoroutineManager.InvokeAfter(() =>
logPropertyChangeCoroutine = CoroutineManager.Invoke(() =>
{
GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
}, delay: 1.0f);
@@ -920,7 +920,8 @@ namespace Barotrauma
CanSpriteFlipY = subElement.GetAttributeBool("canflipy", true);
sprite = new Sprite(subElement, spriteFolder, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null)
if (subElement.Attribute("sourcerect") == null &&
subElement.Attribute("sheetindex") == null)
{
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for item \"" + Name + "\"!");
}
@@ -1152,11 +1153,8 @@ namespace Barotrauma
{
DebugConsole.ThrowError("Error in item prefab \"" + Name + "\" - suitable treatments should be defined using item identifiers, not item names.");
}
string treatmentIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
string treatmentIdentifier = (subElement.GetAttributeString("identifier", null) ?? subElement.GetAttributeString("type", string.Empty)).ToLowerInvariant();
float suitability = subElement.GetAttributeFloat("suitability", 0.0f);
treatmentSuitability.Add(treatmentIdentifier, suitability);
break;
}
@@ -1829,7 +1829,7 @@ namespace Barotrauma
private void GenerateRuin(Point ruinPos, bool mirror)
{
var ruinGenerationParams = RuinGenerationParams.GetRandom();
var ruinGenerationParams = RuinGenerationParams.GetRandom(Rand.RandSync.Server);
LocationType locationType = StartLocation?.Type;
if (locationType == null)
@@ -1839,7 +1839,7 @@ namespace Barotrauma
{
locationType = LocationType.List.Where(lt =>
ruinGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom();
allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server);
}
}
@@ -3594,7 +3594,7 @@ namespace Barotrauma
{
locationType = LocationType.List.Where(lt =>
outpostGenerationParams.AllowedLocationTypes.Any(allowedType =>
allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom();
allowedType.Equals("any", StringComparison.OrdinalIgnoreCase) || lt.Identifier.Equals(allowedType, StringComparison.OrdinalIgnoreCase))).GetRandom(Rand.RandSync.Server);
}
}
@@ -3953,7 +3953,7 @@ namespace Barotrauma
bool TryGetExtraSpawnPoint(out Vector2 point)
{
point = Vector2.Zero;
var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom();
var hull = Hull.hullList.FindAll(h => h.Submarine == wreck).GetRandom(Rand.RandSync.Unsynced);
if (hull != null)
{
point = hull.WorldPosition;
@@ -44,7 +44,7 @@ namespace Barotrauma.RuinGeneration
this.filePath = filePath;
}
public static RuinGenerationParams GetRandom()
public static RuinGenerationParams GetRandom(Rand.RandSync randSync = Rand.RandSync.Server)
{
if (paramsList == null) { LoadAll(); }
@@ -54,7 +54,7 @@ namespace Barotrauma.RuinGeneration
return new RuinGenerationParams(null, null);
}
return paramsList[Rand.Int(paramsList.Count, Rand.RandSync.Server)];
return paramsList[Rand.Int(paramsList.Count, randSync)];
}
private static void LoadAll()
@@ -90,7 +90,7 @@ namespace Barotrauma
private const float StoreMaxReputationModifier = 0.1f;
private const float StoreSellPriceModifier = 0.8f;
private const float DailySpecialPriceModifier = 0.9f;
private const float DailySpecialPriceModifier = 0.5f;
private const float RequestGoodPriceModifier = 1.5f;
public const int StoreInitialBalance = 5000;
/// <summary>
@@ -472,17 +472,18 @@ namespace Barotrauma
foreach (LocationConnection connection in Connections)
{
connection.Difficulty = MathHelper.Clamp((connection.CenterPos.X / Width * 100) + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f);
float difficulty = GetLevelDifficulty(connection.CenterPos.X / Width);
connection.Difficulty = MathHelper.Clamp(difficulty + Rand.Range(-10.0f, 0.0f, Rand.RandSync.Server), 1.2f, 100.0f);
}
AssignBiomes();
CreateEndLocation();
foreach (Location location in Locations)
{
location.LevelData = new LevelData(location)
{
Difficulty = MathHelper.Clamp(location.MapPosition.X / Width * 100, 0.0f, 100.0f)
Difficulty = MathHelper.Clamp(GetLevelDifficulty(location.MapPosition.X / Width), 0.0f, 100.0f)
};
location.UnlockInitialMissions();
}
@@ -490,6 +491,14 @@ namespace Barotrauma
{
connection.LevelData = new LevelData(connection);
}
float GetLevelDifficulty(float areaDifficulty)
{
const float CurveModifier = 1.5f;
const float DifficultyMultiplier = 1.1f;
const float BaseDifficulty = -3f;
return (float)(1 - Math.Pow(1 - areaDifficulty, CurveModifier)) * DifficultyMultiplier * 100f + BaseDifficulty;
}
}
partial void GenerateLocationConnectionVisuals();
@@ -85,7 +85,15 @@ namespace Barotrauma
var subInfo = new SubmarineInfo(outpostModuleFile.Path);
if (subInfo.OutpostModuleInfo != null)
{
if (subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin") != generationParams is RuinGeneration.RuinGenerationParams) { continue; }
if (generationParams is RuinGeneration.RuinGenerationParams)
{
//if the module doesn't have the ruin flag or any other flag used in the generation params, don't use it in ruins
if (!subInfo.OutpostModuleInfo.ModuleFlags.Contains("ruin") &&
!generationParams.ModuleCounts.Any(m => subInfo.OutpostModuleInfo.ModuleFlags.Contains(m.Key)))
{
continue;
}
}
outpostModules.Add(subInfo);
}
}
@@ -11,6 +11,7 @@ using System.Xml.Linq;
using Barotrauma.Abilities;
#if CLIENT
using Microsoft.Xna.Framework.Graphics;
using Barotrauma.Lights;
#endif
namespace Barotrauma
@@ -97,7 +98,7 @@ namespace Barotrauma
{
get { return Prefab.Body; }
}
public List<Body> Bodies { get; private set; }
public bool CastShadow
@@ -113,7 +114,7 @@ namespace Barotrauma
}
private float? maxHealth;
[Serialize(100.0f, true)]
public float MaxHealth
{
@@ -168,7 +169,7 @@ namespace Barotrauma
{
get { return prefab.Tags; }
}
protected Color spriteColor;
[Editable, Serialize("1.0,1.0,1.0,1.0", true)]
public Color SpriteColor
@@ -176,7 +177,7 @@ namespace Barotrauma
get { return spriteColor; }
set { spriteColor = value; }
}
[Editable, Serialize(false, true)]
public bool UseDropShadow
{
@@ -216,6 +217,13 @@ namespace Barotrauma
UpdateSections();
}
}
#if CLIENT
foreach (LightSource light in Lights)
{
light.SpriteScale = scale * textureScale;
}
#endif
}
}
@@ -231,6 +239,13 @@ namespace Barotrauma
textureScale = new Vector2(
MathHelper.Clamp(value.X, 0.01f, 10),
MathHelper.Clamp(value.Y, 0.01f, 10));
#if CLIENT
foreach (LightSource light in Lights)
{
light.LightTextureScale = textureScale * scale;
}
#endif
}
}
@@ -239,7 +254,13 @@ namespace Barotrauma
public Vector2 TextureOffset
{
get { return textureOffset; }
set { textureOffset = value; }
set
{
textureOffset = value;
#if CLIENT
SetLightTextureOffset();
#endif
}
}
@@ -282,10 +303,10 @@ namespace Barotrauma
secRect.X += value.X; secRect.Y += value.Y;
sec.rect = secRect;
}
}
}
}
}
public float BodyWidth
{
get { return Prefab.BodyWidth > 0.0f ? Prefab.BodyWidth * scale : rect.Width; }
@@ -364,6 +385,12 @@ namespace Barotrauma
#if CLIENT
convexHulls?.ForEach(x => x.Move(amount));
foreach (LightSource light in Lights)
{
light.LightTextureTargetSize = rect.Size.ToVector2();
light.Position = rect.Location.ToVector2();
}
#endif
}
@@ -375,7 +402,7 @@ namespace Barotrauma
defaultRect = rectangle;
maxHealth = sp.Health;
rect = rectangle;
TextureScale = sp.TextureScale;
@@ -427,13 +454,48 @@ namespace Barotrauma
if (StairDirection != Direction.None)
{
CreateStairBodies();
}
}
}
}
SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this);
// Only add ai targets automatically to submarine/outpost walls
#if CLIENT
foreach (XElement subElement in sp.ConfigElement.Elements())
{
if (subElement.Name.ToString().Equals("light", StringComparison.OrdinalIgnoreCase))
{
Vector2 pos = rect.Location.ToVector2();
pos.Y += rect.Height;
LightSource light = new LightSource(subElement)
{
ParentSub = Submarine,
Position = rect.Location.ToVector2(),
CastShadows = false,
IsBackground = false,
Color = subElement.GetAttributeColor("lightcolor", Color.White),
SpriteScale = Vector2.One,
Range = 0,
LightTextureTargetSize = rect.Size.ToVector2(),
LightTextureScale = textureScale * scale,
LightSourceParams =
{
Flicker = subElement.GetAttributeFloat("flicker", 0f),
FlickerSpeed = subElement.GetAttributeFloat("flickerspeed", 0f),
PulseAmount = subElement.GetAttributeFloat("pulseamount", 0f),
PulseFrequency = subElement.GetAttributeFloat("pulsefrequency", 0f),
BlinkFrequency = subElement.GetAttributeFloat("blinkfrequency", 0f)
}
};
Lights.Add(light);
SetLightTextureOffset();
}
}
#endif
// Only add ai targets automatically to submarine/outpost walls
if (aiTarget == null && HasBody && Tags.Contains("wall") && submarine != null && !submarine.Info.IsWreck && !NoAITarget)
{
aiTarget = new AITarget(this)
@@ -445,7 +507,7 @@ namespace Barotrauma
}
InsertToList();
DebugConsole.Log("Created " + Name + " (" + ID + ")");
}
@@ -477,7 +539,7 @@ namespace Barotrauma
{
Bodies = new List<Body>();
bodyDebugDimensions.Clear();
float stairAngle = MathHelper.ToRadians(Math.Min(Prefab.StairAngle, 75.0f));
float bodyWidth = ConvertUnits.ToSimUnits(rect.Width / Math.Cos(stairAngle));
@@ -505,9 +567,9 @@ namespace Barotrauma
{
int xsections = 1, ysections = 1;
int width = rect.Width, height = rect.Height;
if (!HasBody)
{
{
if (FlippedX && IsHorizontal)
{
xsections = (int)Math.Ceiling((float)rect.Width / prefab.sprite.SourceRect.Width);
@@ -549,8 +611,8 @@ namespace Barotrauma
if (FlippedX || FlippedY)
{
Rectangle sectionRect = new Rectangle(
FlippedX ? rect.Right - (x + 1) * width : rect.X + x * width,
FlippedY ? rect.Y - rect.Height + (y + 1) * height : rect.Y - y * height,
FlippedX ? rect.Right - (x + 1) * width : rect.X + x * width,
FlippedY ? rect.Y - rect.Height + (y + 1) * height : rect.Y - y * height,
width, height);
if (FlippedX)
@@ -646,8 +708,8 @@ namespace Barotrauma
Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, BodyRotation);
return
Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
return
Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f;
}
else
@@ -693,6 +755,10 @@ namespace Barotrauma
#if CLIENT
if (convexHulls != null) convexHulls.ForEach(x => x.Remove());
foreach (LightSource light in Lights)
{
light.Remove();
}
#endif
}
@@ -725,6 +791,10 @@ namespace Barotrauma
#if CLIENT
if (convexHulls != null) convexHulls.ForEach(x => x.Remove());
foreach (LightSource light in Lights)
{
light.Remove();
}
#endif
}
@@ -782,7 +852,7 @@ namespace Barotrauma
return (IsHorizontal ? Sections[sectionIndex].rect.Width : Sections[sectionIndex].rect.Height);
}
public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false)
{
if (!upgrade.Prefab.IsWallUpgrade) { return false; }
@@ -800,7 +870,7 @@ namespace Barotrauma
Upgrades.Add(upgrade);
upgrade.ApplyUpgrade();
}
UpdateSections();
return true;
@@ -915,7 +985,7 @@ namespace Barotrauma
{
diffFromCenter = -diffFromCenter;
}
Vector2 sectionPos = Position + new Vector2(
(float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
(float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter;
@@ -926,7 +996,7 @@ namespace Barotrauma
}
return sectionPos;
}
}
}
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false)
{
@@ -949,7 +1019,7 @@ namespace Barotrauma
GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f);
#endif
}
}
}
#if CLIENT
if (playSound && damageAmount > 0)
{
@@ -976,7 +1046,7 @@ namespace Barotrauma
if (!MathUtils.IsValid(damage)) { return; }
damage = MathHelper.Clamp(damage, 0.0f, MaxHealth - Prefab.MinHealth);
#if SERVER
if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage)
{
@@ -1022,10 +1092,10 @@ namespace Barotrauma
{
diffFromCenter = (gapRect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth;
if (BodyWidth > 0.0f) { gapRect.Width = (int)(BodyWidth * (gapRect.Width / (float)this.rect.Width)); }
if (BodyHeight > 0.0f)
{
if (BodyHeight > 0.0f)
{
gapRect.Y = (gapRect.Y - gapRect.Height / 2) + (int)(BodyHeight / 2 + BodyOffset.Y * scale);
gapRect.Height = (int)BodyHeight;
gapRect.Height = (int)BodyHeight;
}
}
else
@@ -1034,7 +1104,7 @@ namespace Barotrauma
if (BodyWidth > 0.0f)
{
gapRect.X = gapRect.Center.X + (int)(-BodyWidth / 2 + BodyOffset.X * scale);
gapRect.Width = (int)BodyWidth;
gapRect.Width = (int)BodyWidth;
}
if (BodyHeight > 0.0f) { gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height)); }
}
@@ -1053,7 +1123,7 @@ namespace Barotrauma
gapRect.Y += 10;
gapRect.Width += 20;
gapRect.Height += 20;
bool horizontalGap = !IsHorizontal;
if (Prefab.BodyRotation != 0.0f)
{
@@ -1090,7 +1160,7 @@ namespace Barotrauma
}
float gapOpen = MaxHealth <= 0.0f ? 0.0f : (damage / MaxHealth - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
Sections[sectionIndex].gap.Open = gapOpen;
Sections[sectionIndex].gap.Open = gapOpen;
}
float damageDiff = damage - Sections[sectionIndex].damage;
@@ -1106,9 +1176,9 @@ namespace Barotrauma
{
if (damageDiff < 0.0f)
{
attacker.Info?.IncreaseSkillLevel("mechanical",
attacker.Info?.IncreaseSkillLevel("mechanical",
-damageDiff * SkillSettings.Current.SkillIncreasePerRepairedStructureDamage / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f),
SectionPosition(sectionIndex));
SectionPosition(sectionIndex));
}
}
}
@@ -1116,7 +1186,7 @@ namespace Barotrauma
bool hasHole = SectionBodyDisabled(sectionIndex);
if (hadHole == hasHole) { return; }
UpdateSections();
}
@@ -1198,7 +1268,7 @@ namespace Barotrauma
if (BodyHeight > 0.0f) rect.Height = Math.Max((int)Math.Round(BodyHeight * (rect.Height / (float)this.rect.Height)), 1);
}
if (FlippedX) { diffFromCenter = -diffFromCenter; }
Vector2 bodyOffset = ConvertUnits.ToSimUnits(Prefab.BodyOffset) * scale;
if (FlippedX) { bodyOffset.X = -bodyOffset.X; }
if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; }
@@ -1240,7 +1310,7 @@ namespace Barotrauma
}
partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation);
public override void FlipX(bool relativeToSub)
{
base.FlipX(relativeToSub);
@@ -1261,7 +1331,7 @@ namespace Barotrauma
CreateStairBodies();
}
if (HasBody)
{
CreateSections();
@@ -1388,7 +1458,7 @@ namespace Barotrauma
StructurePrefab prefab = null;
if (string.IsNullOrEmpty(identifier))
{
//legacy support:
//legacy support:
//1. attempt to find a prefab with an empty identifier and a matching name
prefab = MapEntityPrefab.Find(name, "") as StructurePrefab;
//2. not found, attempt to find a prefab with a matching name
@@ -1433,7 +1503,7 @@ namespace Barotrauma
}
SerializableProperty.SerializeProperties(this, element);
foreach (var upgrade in Upgrades)
{
upgrade.Save(element);
@@ -291,7 +291,8 @@ namespace Barotrauma
{
case "sprite":
sp.sprite = new Sprite(subElement, lazyLoad: true);
if (subElement.Attribute("sourcerect") == null)
if (subElement.Attribute("sourcerect") == null &&
subElement.Attribute("sheetindex") == null)
{
DebugConsole.ThrowError("Warning - sprite sourcerect not configured for structure \"" + sp.name + "\"!");
}
@@ -914,9 +914,11 @@ namespace Barotrauma
mapEntity.Move(-HiddenSubPosition);
}
var prevBodyType = subBody.Body.BodyType;
Vector2 pos = new Vector2(subBody.Position.X, subBody.Position.Y);
subBody.Body.Remove();
subBody = new SubmarineBody(this);
subBody.Body.BodyType = prevBodyType;
SetPosition(pos, new List<Submarine>(parents.Where(p => p != this)));
if (entityGrid != null)
@@ -1429,6 +1431,11 @@ namespace Barotrauma
}
}
}
else if (info.IsRuin)
{
ShowSonarMarker = false;
PhysicsBody.FarseerBody.BodyType = BodyType.Static;
}
}
if (entityGrid != null)
@@ -868,11 +868,12 @@ namespace Barotrauma
}
}
public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false)
public static WayPoint GetRandom(SpawnType spawnType = SpawnType.Human, JobPrefab assignedJob = null, Submarine sub = null, bool useSyncedRand = false, string spawnPointTag = null)
{
return WayPointList.GetRandom(wp =>
wp.Submarine == sub &&
wp.spawnType == spawnType &&
(string.IsNullOrEmpty(spawnPointTag) || wp.Tags.Any(t => t.Equals(spawnPointTag, StringComparison.OrdinalIgnoreCase))) &&
(assignedJob == null || (assignedJob != null && wp.AssignedJob == assignedJob)),
useSyncedRand ? Rand.RandSync.Server : Rand.RandSync.Unsynced);
}
@@ -92,7 +92,7 @@ namespace Barotrauma
}
case ConditionType.AllowRotating:
{
return entity is Item item && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen;
return entity is Item item && item.body == null && item.Prefab.AllowRotatingInEditor && Screen.Selected == GameMain.SubEditorScreen;
}
}
return false;
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -15,6 +16,22 @@ namespace Barotrauma
{
public static class XMLExtensions
{
private static ImmutableDictionary<Type, Func<string, object, object>> converters
= new Dictionary<Type, Func<string, object, object>>()
{
{ typeof(string), (str, defVal) => str },
{ typeof(int), (str, defVal) => int.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out int result) ? result : defVal },
{ typeof(uint), (str, defVal) => uint.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out uint result) ? result : defVal },
{ typeof(UInt64), (str, defVal) => UInt64.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out UInt64 result) ? result : defVal },
{ typeof(float), (str, defVal) => float.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out float result) ? result : defVal },
{ typeof(bool), (str, defVal) => bool.TryParse(str, out bool result) ? result : defVal },
{ typeof(Color), (str, defVal) => ParseColor(str) },
{ typeof(Vector2), (str, defVal) => ParseVector2(str) },
{ typeof(Vector3), (str, defVal) => ParseVector3(str) },
{ typeof(Vector4), (str, defVal) => ParseVector4(str) },
{ typeof(Rectangle), (str, defVal) => ParseRect(str, true) }
}.ToImmutableDictionary();
public static string ParseContentPathFromUri(this XObject element) => System.IO.Path.GetRelativePath(Environment.CurrentDirectory, element.BaseUri);
public static readonly XmlReaderSettings ReaderSettings = new XmlReaderSettings
@@ -485,6 +502,25 @@ namespace Barotrauma
return ParseRect(element.Attribute(name).Value, false);
}
//TODO: nested tuples and and n-uples where n!=2 are unsupported
public static (T1, T2) GetAttributeTuple<T1, T2>(this XElement element, string name, (T1, T2) defaultValue)
{
string strValue = element.GetAttributeString(name, $"({defaultValue.Item1}, {defaultValue.Item2})").Trim();
return ParseTuple(strValue, defaultValue);
}
public static (T1, T2)[] GetAttributeTupleArray<T1, T2>(this XElement element, string name,
(T1, T2)[] defaultValue)
{
if (element?.Attribute(name) == null) { return defaultValue; }
string stringValue = element.Attribute(name).Value;
if (string.IsNullOrEmpty(stringValue)) { return defaultValue; }
return stringValue.Split(';').Select(s => ParseTuple<T1, T2>(s, default)).ToArray();
}
public static string ElementInnerText(this XElement el)
{
StringBuilder str = new StringBuilder();
@@ -525,12 +561,28 @@ namespace Barotrauma
=> $"{color.R},{color.G},{color.B},{color.A}";
public static string ToStringHex(this Color color)
=> $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
=> $"#{color.R:X2}{color.G:X2}{color.B:X2}"
+ ((color.A < 255) ? $"{color.A:X2}" : "");
public static string RectToString(Rectangle rect)
{
return rect.X + "," + rect.Y + "," + rect.Width + "," + rect.Height;
}
public static (T1, T2) ParseTuple<T1, T2>(string strValue, (T1, T2) defaultValue)
{
strValue = strValue.Trim();
//require parentheses
if (strValue[0] != '(' || strValue[^1] != ')') { return defaultValue; }
//remove parentheses
strValue = strValue[1..^1];
string[] elems = strValue.Split(',');
if (elems.Length != 2) { return defaultValue; }
return ((T1)converters[typeof(T1)].Invoke(elems[0], defaultValue.Item1),
(T2)converters[typeof(T2)].Invoke(elems[1], defaultValue.Item2));
}
public static Point ParsePoint(string stringPoint, bool errorMessages = true)
{
@@ -197,6 +197,18 @@ namespace Barotrauma
}
}
public class GiveTalentInfo
{
public string[] TalentIdentifiers;
public bool GiveRandom;
public GiveTalentInfo(XElement element, string parentDebugName)
{
TalentIdentifiers = element.GetAttributeStringArray("talentidentifiers", new string[0], convertToLowerInvariant: true);
GiveRandom = element.GetAttributeBool("giverandom", false);
}
}
public class CharacterSpawnInfo : ISerializableEntity
{
public string Name => $"Character Spawn Info ({SpeciesName})";
@@ -274,6 +286,8 @@ namespace Barotrauma
private readonly bool spawnItemRandomly;
private readonly List<CharacterSpawnInfo> spawnCharacters;
public readonly List<GiveTalentInfo> giveTalentInfos;
private readonly List<AITrigger> aiTriggers;
private readonly List<EventPrefab> triggeredEvents;
@@ -374,6 +388,7 @@ namespace Barotrauma
spawnItems = new List<ItemSpawnInfo>();
spawnItemRandomly = element.GetAttributeBool("spawnitemrandomly", false);
spawnCharacters = new List<CharacterSpawnInfo>();
giveTalentInfos = new List<GiveTalentInfo>();
aiTriggers = new List<AITrigger>();
Afflictions = new List<Affliction>();
Explosions = new List<Explosion>();
@@ -576,7 +591,7 @@ namespace Barotrauma
break;
case "requiredaffliction":
requiredAfflictions ??= new HashSet<(string, float)>();
string[] ids = subElement.GetAttributeStringArray("identifier", new string[0]);
string[] ids = subElement.GetAttributeStringArray("identifier", null) ?? subElement.GetAttributeStringArray("type", new string[0]);
foreach (string afflictionId in ids)
{
requiredAfflictions.Add((
@@ -669,6 +684,10 @@ namespace Barotrauma
var newSpawnCharacter = new CharacterSpawnInfo(subElement, parentDebugName);
if (!string.IsNullOrWhiteSpace(newSpawnCharacter.SpeciesName)) { spawnCharacters.Add(newSpawnCharacter); }
break;
case "givetalentinfo":
var newGiveTalentInfo = new GiveTalentInfo(subElement, parentDebugName);
if (newGiveTalentInfo.TalentIdentifiers.Any()) { giveTalentInfos.Add(newGiveTalentInfo); }
break;
case "aitrigger":
aiTriggers.Add(new AITrigger(subElement));
break;
@@ -1305,6 +1324,33 @@ namespace Barotrauma
}
}
}
if (giveTalentInfos.Any() && (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient))
{
Character targetCharacter = CharacterFromTarget(target);
if (targetCharacter?.Info == null) { continue; }
if (!TalentTree.JobTalentTrees.TryGetValue(targetCharacter.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { continue; }
// for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well
IEnumerable<string> disallowedTalents = talentTree.TalentSubTrees.SelectMany(s => s.TalentOptionStages.SelectMany(o => o.Talents.Select(t => t.Identifier)));
foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos)
{
IEnumerable<string> viableTalents = giveTalentInfo.TalentIdentifiers.Where(s => !targetCharacter.Info.UnlockedTalents.Contains(s) && !disallowedTalents.Contains(s));
if (viableTalents.None()) { continue; }
if (giveTalentInfo.GiveRandom)
{
targetCharacter.GiveTalent(viableTalents.GetRandom(), true);
}
else
{
foreach (string talent in viableTalents)
{
targetCharacter.GiveTalent(talent, true);
}
}
}
}
}
if (FireSize > 0.0f && entity != null)
@@ -1367,6 +1413,7 @@ namespace Barotrauma
});
}
}
if (spawnItemRandomly)
{
SpawnItem(spawnItems.GetRandom());
@@ -147,7 +147,7 @@ namespace Barotrauma
/// <returns></returns>
private static float ApplyPercentage(float value, float amount, int times)
{
return times <= 0 ? value : ApplyPercentage(value + (value * amount / 100), amount, --times);
return (1f + (amount / 100f * times)) * value;
}
public static PropertyReference[] ParseAttributes(IEnumerable<XAttribute> attributes, Upgrade upgrade)
@@ -68,7 +68,13 @@ namespace Barotrauma
Name = element.GetAttributeString("name", string.Empty);
IsWallUpgrade = element.GetAttributeBool("wallupgrade", false);
if (string.IsNullOrWhiteSpace(Name))
string nameIdentifier = element.GetAttributeString("nameidentifier", "");
if (!string.IsNullOrWhiteSpace(nameIdentifier))
{
Name = TextManager.Get($"{nameIdentifier}", returnNull: true) ?? string.Empty;
}
else if (string.IsNullOrWhiteSpace(Name))
{
Name = TextManager.Get($"UpgradeCategory.{Identifier}", true) ?? string.Empty;
}
@@ -131,6 +137,8 @@ namespace Barotrauma
public string Description { get; }
public float IncreaseOnTooltip { get; }
public string Identifier { get; }
public string FilePath { get; }
@@ -172,7 +180,13 @@ namespace Barotrauma
var targetProperties = new Dictionary<string, string[]>();
if (string.IsNullOrWhiteSpace(Name))
string nameIdentifier = element.GetAttributeString("nameidentifier", "");
if (!string.IsNullOrWhiteSpace(nameIdentifier))
{
Name = TextManager.Get($"UpgradeName.{nameIdentifier}", returnNull: true) ?? string.Empty;
}
else if (string.IsNullOrWhiteSpace(Name))
{
Name = TextManager.Get($"UpgradeName.{Identifier}", returnNull: true) ?? string.Empty;
}
@@ -182,6 +196,8 @@ namespace Barotrauma
Description = TextManager.Get($"UpgradeDescription.{Identifier}", returnNull: true) ?? string.Empty;
}
IncreaseOnTooltip = element.GetAttributeFloat("increaseontooltip", 0f);
DebugConsole.Log(" " + Name);
foreach (XElement subElement in element.Elements())