Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop
This commit is contained in:
@@ -500,7 +500,8 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server?.ServerSettings?.RespawnMode == RespawnMode.Permadeath)
|
||||
if (GameMain.Server?.ServerSettings?.RespawnMode == RespawnMode.Permadeath &&
|
||||
causeOfDeath.Type != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
UnlockAchievement(character, "abyssbeckons".ToIdentifier());
|
||||
}
|
||||
|
||||
@@ -213,7 +213,8 @@ namespace Barotrauma
|
||||
if (targetHull == null) { return false; }
|
||||
if (maxDistance > 0)
|
||||
{
|
||||
if (Vector2.DistanceSquared(Character.WorldPosition, targetWorldPos) > maxDistance * maxDistance) { return false; }
|
||||
// Too far from the gap.
|
||||
if (Vector2.DistanceSquared(Character.WorldPosition, gap.WorldPosition) > maxDistance * maxDistance) { return false; }
|
||||
}
|
||||
if (SteeringManager is IndoorsSteeringManager pathSteering)
|
||||
{
|
||||
@@ -325,7 +326,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (dropOtherIfCannotMove)
|
||||
{
|
||||
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
|
||||
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || (targetTags != null && otherItem.HasIdentifierOrTags(targetTags)))
|
||||
{
|
||||
bool switchingToBetterSuit =
|
||||
targetTags != null &&
|
||||
@@ -358,13 +359,23 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea);
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
public void UnequipEmptyItems(Item parentItem, bool avoidDroppingInSea = true, bool allowDestroying = false) => UnequipEmptyItems(Character, parentItem, avoidDroppingInSea, allowDestroying);
|
||||
|
||||
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, unequipMax);
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
/// <param name="unequipMax">Optional max amount for items to be unequipped.</param>
|
||||
public void UnequipContainedItems(Item parentItem, Func<Item, bool> predicate = null, bool avoidDroppingInSea = true, bool allowDestroying = false, int? unequipMax = null) => UnequipContainedItems(Character, parentItem, predicate, avoidDroppingInSea, allowDestroying, unequipMax);
|
||||
|
||||
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea);
|
||||
|
||||
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
public static void UnequipEmptyItems(Character character, Item parentItem, bool avoidDroppingInSea = true, bool allowDestroying = false) => UnequipContainedItems(character, parentItem, it => it.Condition <= 0, avoidDroppingInSea, allowDestroying);
|
||||
|
||||
/// <param name="avoidDroppingInSea">When enabled, items are first put in the inventory and dropped only if that fails, unless the character is inside a friendly submarine.</param>
|
||||
/// <param name="allowDestroying">Allows destroying of the items when unequipped (instead of dropping them). Used only with infinite spawns.</param>
|
||||
/// <param name="unequipMax">Optional max amount for items to be unequipped.</param>
|
||||
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, bool allowDestroying = false, int? unequipMax = null)
|
||||
{
|
||||
var inventory = parentItem.OwnInventory;
|
||||
if (inventory == null || !inventory.Container.DrawInventory) { return; }
|
||||
@@ -376,21 +387,36 @@ namespace Barotrauma
|
||||
if (containedItem == null) { continue; }
|
||||
if (predicate == null || predicate(containedItem))
|
||||
{
|
||||
if (avoidDroppingInSea && !character.IsInFriendlySub)
|
||||
if (allowDestroying && GameMain.NetworkMember is not { IsClient: true } && character.AIController.HasInfiniteItemSpawns(containedItem.Prefab.Identifier))
|
||||
{
|
||||
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
|
||||
{
|
||||
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
|
||||
continue;
|
||||
}
|
||||
Entity.Spawner?.AddItemToRemoveQueue(containedItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (avoidDroppingInSea && !character.IsInFriendlySub)
|
||||
{
|
||||
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
|
||||
{
|
||||
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
|
||||
continue;
|
||||
}
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
}
|
||||
containedItem.Drop(character);
|
||||
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasInfiniteItemSpawns(IEnumerable<Identifier> itemIdentifiers)
|
||||
=> (Character.HumanPrefab?.InfiniteItems.Any(it => itemIdentifiers.Contains(it.Identifier) || it.Tags.Any(itemIdentifiers.Contains)) ?? false)
|
||||
|| (Character.Info?.Job?.HasJobItem(jobItem => jobItem.Infinite && itemIdentifiers.Contains(jobItem.GetItemIdentifier(Character.TeamID, isPvPMode: GameMain.GameSession.GameMode is PvPMode))) ?? false);
|
||||
|
||||
public bool HasInfiniteItemSpawns(Identifier itemIdentifier)
|
||||
=> (Character.HumanPrefab?.InfiniteItems.Any(it => it.Identifier == itemIdentifier || it.Tags.Contains(itemIdentifier)) ?? false)
|
||||
|| (Character.Info?.Job?.HasJobItem(jobItem => jobItem.Infinite && jobItem.GetItemIdentifier(Character.TeamID, isPvPMode: GameMain.GameSession.GameMode is PvPMode) == itemIdentifier) ?? false);
|
||||
|
||||
public void ReequipUnequipped()
|
||||
{
|
||||
|
||||
@@ -1128,9 +1128,9 @@ namespace Barotrauma
|
||||
searchingNewHull = false;
|
||||
}
|
||||
}
|
||||
if (patrolTarget != null && pathSteering.CurrentPath != null && !pathSteering.CurrentPath.Finished && !pathSteering.CurrentPath.Unreachable)
|
||||
if (patrolTarget != null && pathSteering.CurrentPath is { Finished: false, Unreachable: false })
|
||||
{
|
||||
PathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: n => PatrolNodeFilter(n));
|
||||
pathSteering.SteeringSeek(Character.GetRelativeSimPosition(patrolTarget), weight: 1, minGapWidth: minGapSize * 1.5f, nodeFilter: PatrolNodeFilter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1570,22 +1570,22 @@ namespace Barotrauma
|
||||
Vector2 toTarget = attackWorldPos - attackLimbPos;
|
||||
Vector2 toTargetOffset = toTarget;
|
||||
// Add a margin when the target is moving away, because otherwise it might be difficult to reach it if the attack takes some time to execute
|
||||
if (wallTarget != null && Character.Submarine == null)
|
||||
if (Character.Submarine == null)
|
||||
{
|
||||
if (wallTarget.Structure.Submarine != null)
|
||||
if (wallTarget != null)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
|
||||
if (wallTarget.Structure.Submarine != null)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(wallTarget.Structure.Submarine.Velocity);
|
||||
toTargetOffset += margin;
|
||||
}
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
|
||||
toTargetOffset += margin;
|
||||
}
|
||||
}
|
||||
else if (targetCharacter != null)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(targetCharacter.AnimController.Collider.LinearVelocity);
|
||||
toTargetOffset += margin;
|
||||
}
|
||||
else if (SelectedAiTarget.Entity is MapEntity e)
|
||||
{
|
||||
if (e.Submarine != null)
|
||||
else if (SelectedAiTarget.Entity is MapEntity { Submarine: not null } e)
|
||||
{
|
||||
Vector2 margin = CalculateMargin(e.Submarine.Velocity);
|
||||
toTargetOffset += margin;
|
||||
@@ -1666,12 +1666,15 @@ namespace Barotrauma
|
||||
// Crouch if the target is down (only humanoids), so that we can reach it.
|
||||
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
|
||||
{
|
||||
if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
|
||||
if (targetCharacter?.CurrentHull is Hull targetHull && targetHull == Character.CurrentHull && toTarget.Y < 0)
|
||||
{
|
||||
humanoidAnimController.Crouch();
|
||||
if (Math.Abs(toTarget.Y) > AttackLimb.attack.Range / 2 && Math.Abs(toTarget.X) <= AttackLimb.attack.Range)
|
||||
{
|
||||
humanoidAnimController.Crouch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (canAttack)
|
||||
{
|
||||
if (AttackLimb.attack.Ranged)
|
||||
@@ -1706,6 +1709,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (wallTarget == null && Character.CurrentHull != null && targetCharacter != null)
|
||||
{
|
||||
canAttack = Submarine.PickBody(SimPosition, attackSimPos, collisionCategory: Physics.CollisionWall) == null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Limb steeringLimb = canAttack && !AttackLimb.attack.Ranged ? AttackLimb : null;
|
||||
@@ -1720,6 +1727,8 @@ namespace Barotrauma
|
||||
State = AIState.Idle;
|
||||
return;
|
||||
}
|
||||
// Note that this returns null when we don't currently use IndoorsSteeringManager, even if we are able to path!
|
||||
// -> Don't use PathSteering, because that returns the reference even if we currently don't use it.
|
||||
var pathSteering = SteeringManager as IndoorsSteeringManager;
|
||||
if (AttackLimb != null && AttackLimb.attack.Retreat)
|
||||
{
|
||||
@@ -1727,58 +1736,71 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 steerPos = attackSimPos;
|
||||
if (!Character.AnimController.SimplePhysicsEnabled)
|
||||
{
|
||||
// Offset so that we don't overshoot the movement
|
||||
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
|
||||
steerPos += offset;
|
||||
}
|
||||
if (pathSteering != null)
|
||||
{
|
||||
if (pathSteering.CurrentPath != null)
|
||||
// Attack doors
|
||||
if (canAttackDoors && HasValidPath())
|
||||
{
|
||||
// Attack doors
|
||||
if (canAttackDoors)
|
||||
// If the target is in the same hull, there shouldn't be any doors blocking the path
|
||||
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
|
||||
{
|
||||
// If the target is in the same hull, there shouldn't be any doors blocking the path
|
||||
if (targetCharacter == null || targetCharacter.CurrentHull != Character.CurrentHull)
|
||||
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
|
||||
if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character)))
|
||||
{
|
||||
var door = pathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? pathSteering.CurrentPath.NextNode?.ConnectedDoor;
|
||||
if (door is { CanBeTraversed: false } && (!Character.IsInFriendlySub || !door.HasAccess(Character)))
|
||||
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
|
||||
{
|
||||
if (door.Item.AiTarget != null && SelectedAiTarget != door.Item.AiTarget)
|
||||
{
|
||||
SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
return;
|
||||
}
|
||||
SelectTarget(door.Item.AiTarget, currentTargetMemory.Priority);
|
||||
State = AIState.Attack;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// When pursuing, we don't want to pursue too close
|
||||
float max = 300;
|
||||
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
|
||||
if (!canAttack || distance > margin)
|
||||
}
|
||||
// When pursuing, we don't want to pursue too close
|
||||
float max = 300;
|
||||
float margin = AttackLimb != null ? Math.Min(AttackLimb.attack.Range * 0.9f, max) : max;
|
||||
if ((!canAttack || distance > margin) && !IsTryingToSteerThroughGap)
|
||||
{
|
||||
// Steer towards the target if in the same room and swimming
|
||||
// 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))
|
||||
{
|
||||
// Steer towards the target if in the same room and swimming
|
||||
// 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(attackSimPos - myPos));
|
||||
}
|
||||
else
|
||||
{
|
||||
Func<PathNode, bool> nodeFilter = null;
|
||||
float outsideNodePenalty = 0;
|
||||
if (Character.CurrentHull != null && Character.IsInPlayerSub)
|
||||
{
|
||||
Vector2 myPos = Character.AnimController.SimplePhysicsEnabled ? Character.SimPosition : steeringLimb.SimPosition;
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(steerPos - myPos));
|
||||
// Prefer not to path back outside, if we are inside player sub.
|
||||
// Fixes monsters sometimes pathing from the airlock to another airlock on the other side of the sub, because the path is technically cheaper than the path through the interiors.
|
||||
outsideNodePenalty = 50;
|
||||
}
|
||||
else
|
||||
{
|
||||
pathSteering.SteeringSeek(steerPos, weight: 2,
|
||||
minGapWidth: minGapSize,
|
||||
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null),
|
||||
checkVisiblity: true);
|
||||
pathSteering.SteeringSeek(Character.GetRelativeSimPosition(SelectedAiTarget.Entity), weight: 2,
|
||||
minGapWidth: minGapSize,
|
||||
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (Character.CurrentHull == null),
|
||||
nodeFilter: nodeFilter,
|
||||
checkVisiblity: true,
|
||||
outsideNodePenalty: outsideNodePenalty);
|
||||
|
||||
if (!pathSteering.IsPathDirty && pathSteering.CurrentPath.Unreachable)
|
||||
if (pathSteering.CurrentPath != null)
|
||||
{
|
||||
if (pathSteering.IsPathDirty)
|
||||
{
|
||||
if (Character.CurrentHull is Hull hull && hull.ConnectedGaps.Any(static g => !g.IsRoomToRoom && g.Open >= 1.0f && g.ConnectedDoor != null))
|
||||
{
|
||||
// Reset in the airlock, because otherwise the character may be too slow to change the steering and keep moving outside.
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
// Steer towards the target
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
|
||||
}
|
||||
else if (pathSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
State = AIState.Idle;
|
||||
IgnoreTarget(SelectedAiTarget);
|
||||
@@ -1787,39 +1809,42 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!IsTryingToSteerThroughGap)
|
||||
}
|
||||
else if (!IsTryingToSteerThroughGap)
|
||||
{
|
||||
if (AttackLimb.attack.Ranged)
|
||||
{
|
||||
if (AttackLimb.attack.Ranged)
|
||||
float dir = Character.AnimController.Dir;
|
||||
if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
|
||||
{
|
||||
float dir = Character.AnimController.Dir;
|
||||
if (dir > 0 && attackWorldPos.X > AttackLimb.WorldPosition.X + margin || dir < 0 && attackWorldPos.X < AttackLimb.WorldPosition.X - margin)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Too close
|
||||
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
|
||||
}
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Close enough
|
||||
SteeringManager.Reset();
|
||||
// Too close
|
||||
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
|
||||
// Close enough
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pathSteering.SteeringSeek(steerPos, weight: 5, minGapWidth: minGapSize);
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(SelectedAiTarget.Entity.WorldPosition - Character.WorldPosition));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 steerPos = attackSimPos;
|
||||
if (!Character.AnimController.SimplePhysicsEnabled)
|
||||
{
|
||||
// Offset so that we don't overshoot the movement
|
||||
Vector2 offset = Character.SimPosition - steeringLimb.SimPosition;
|
||||
steerPos += offset;
|
||||
}
|
||||
// Sweeping and circling doesn't work well inside
|
||||
if (Character.CurrentHull == null)
|
||||
{
|
||||
@@ -2232,7 +2257,7 @@ namespace Barotrauma
|
||||
// Don't allow root motion attacks, if we are not on the same level with the target, because it can cause warping.
|
||||
switch (target)
|
||||
{
|
||||
case Character targetCharacter when Character.CurrentHull != targetCharacter.CurrentHull || targetCharacter.IsKnockedDown:
|
||||
case Character targetCharacter when Character.CurrentHull != targetCharacter.CurrentHull || targetCharacter.IsKnockedDownOrRagdolled:
|
||||
case Item targetItem when Character.CurrentHull != targetItem.CurrentHull:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ namespace Barotrauma
|
||||
enemyCheckTimer -= deltaTime;
|
||||
if (enemyCheckTimer < 0)
|
||||
{
|
||||
CheckEnemies();
|
||||
SpotEnemies();
|
||||
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
|
||||
}
|
||||
}
|
||||
@@ -443,7 +443,6 @@ namespace Barotrauma
|
||||
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
|
||||
{
|
||||
ReportProblems();
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -467,31 +466,7 @@ namespace Barotrauma
|
||||
bool run = !currentObjective.ForceWalk && (currentObjective.ForceRun || objectiveManager.GetCurrentPriority() > AIObjectiveManager.RunPriority);
|
||||
if (currentObjective is AIObjectiveGoTo goTo)
|
||||
{
|
||||
if (run && goTo == objectiveManager.ForcedOrder && goTo.IsWaitOrder && !Character.IsOnPlayerTeam)
|
||||
{
|
||||
// NPCs with a wait order don't run.
|
||||
run = false;
|
||||
}
|
||||
else if (goTo.Target != null)
|
||||
{
|
||||
if (Character.CurrentHull == null)
|
||||
{
|
||||
run = Vector2.DistanceSquared(Character.WorldPosition, goTo.Target.WorldPosition) > 300 * 300;
|
||||
}
|
||||
else
|
||||
{
|
||||
float yDiff = goTo.Target.WorldPosition.Y - Character.WorldPosition.Y;
|
||||
if (Math.Abs(yDiff) > 100)
|
||||
{
|
||||
run = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
|
||||
run = Math.Abs(xDiff) > 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
run = goTo.ShouldRun(run);
|
||||
}
|
||||
|
||||
//if someone is grabbing the bot and the bot isn't trying to run anywhere, let them keep dragging and "control" the bot
|
||||
@@ -564,13 +539,15 @@ namespace Barotrauma
|
||||
ShipCommandManager?.Update(deltaTime);
|
||||
}
|
||||
|
||||
private void CheckEnemies()
|
||||
private void SpotEnemies()
|
||||
{
|
||||
//already in combat, no need to check
|
||||
if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
|
||||
if (objectiveManager.HasActiveObjective<AIObjectiveCombat>()) { return; }
|
||||
|
||||
float closestDistance = 0;
|
||||
Character closestEnemy = null;
|
||||
bool shouldActOffensively = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Submarine != Character.Submarine) { continue; }
|
||||
@@ -596,8 +573,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (closestEnemy != null)
|
||||
{
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
|
||||
}
|
||||
AddCombatObjective(shouldActOffensively ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnequipUnnecessaryItems()
|
||||
@@ -606,7 +583,7 @@ namespace Barotrauma
|
||||
if (ObjectiveManager.CurrentObjective == null) { return; }
|
||||
if (Character.CurrentHull == null) { return; }
|
||||
bool shouldActOnSuffocation = Character.IsLowInOxygen && !Character.AnimController.HeadInWater && HasDivingSuit(Character, requireOxygenTank: false) && !HasItem(Character, Tags.OxygenSource, out _, conditionPercentage: 1);
|
||||
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
|
||||
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveMoveItem>();
|
||||
|
||||
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
|
||||
{
|
||||
@@ -621,8 +598,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (findItemState != FindItemState.OtherItem)
|
||||
{
|
||||
var decontain = ObjectiveManager.GetActiveObjectives<AIObjectiveDecontainItem>().LastOrDefault();
|
||||
if (decontain != null && decontain.TargetItem != null && decontain.TargetItem.HasTag(Tags.HeavyDivingGear) &&
|
||||
var moveItemObjective = ObjectiveManager.GetLastActiveObjective<AIObjectiveMoveItem>();
|
||||
if (moveItemObjective is { TargetItem: not null } && moveItemObjective.TargetItem.HasTag(Tags.HeavyDivingGear) &&
|
||||
ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
|
||||
{
|
||||
// Don't try to put the diving suit in a locker if the suit would be needed in any hull in the path to the locker.
|
||||
@@ -728,17 +705,17 @@ namespace Barotrauma
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
|
||||
var moveItemObjective = new AIObjectiveMoveItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
DropIfFails = false
|
||||
};
|
||||
decontainObjective.Abandoned += () =>
|
||||
moveItemObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
decontainObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
moveItemObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -764,7 +741,7 @@ namespace Barotrauma
|
||||
HandleRelocation(mask);
|
||||
ReequipUnequipped();
|
||||
}
|
||||
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
|
||||
else if (findItemState is FindItemState.None or FindItemState.DivingMask)
|
||||
{
|
||||
findItemState = FindItemState.DivingMask;
|
||||
if (FindSuitableContainer(mask, out Item targetContainer))
|
||||
@@ -773,14 +750,14 @@ namespace Barotrauma
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
var moveItemObjective = new AIObjectiveMoveItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
moveItemObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
decontainObjective.Completed += () => ReequipUnequipped();
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
moveItemObjective.Completed += ReequipUnequipped;
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -814,9 +791,9 @@ namespace Barotrauma
|
||||
if (Character.TryPutItemInBag(item)) { continue; }
|
||||
if (item.HasTag(Tags.Weapon))
|
||||
{
|
||||
// Don't decontain weapons, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in the any slot, such as an HMG.
|
||||
// Don't store weapons in containers, because it could be that we are holding a weapon that cannot be placed on back (if we have a toolbelt) nor in any slot, such as an HMG.
|
||||
// Could check that we only ignore weapons when we've had an order to find a weapon, but it could also be that we picked the weapon for self-defence, on ad-hoc basis.
|
||||
// And I don't think it would make sense to decontain those weapons either.
|
||||
// And I don't think it would make sense to move those weapons in containers either.
|
||||
continue;
|
||||
}
|
||||
findItemState = FindItemState.OtherItem;
|
||||
@@ -826,13 +803,13 @@ namespace Barotrauma
|
||||
itemIndex = 0;
|
||||
if (targetContainer != null)
|
||||
{
|
||||
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
decontainObjective.Abandoned += () =>
|
||||
var moveItemObjective = new AIObjectiveMoveItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
|
||||
moveItemObjective.Abandoned += () =>
|
||||
{
|
||||
ReequipUnequipped();
|
||||
IgnoredItems.Add(targetContainer);
|
||||
};
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
|
||||
ObjectiveManager.CurrentObjective.AddSubObjective(moveItemObjective, addFirst: true);
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -1046,12 +1023,12 @@ namespace Barotrauma
|
||||
foreach (Character target in Character.CharacterList)
|
||||
{
|
||||
if (target.CurrentHull != hull || !target.Enabled || target.InDetectable) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, targetCharactersInOtherSubs: false))
|
||||
{
|
||||
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
if (AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
if (target.IsEscorted)
|
||||
{
|
||||
@@ -1069,6 +1046,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Character.CombatAction == null && !isFighting)
|
||||
{
|
||||
// Immediately react to enemies when they are spotted. AIObjectiveFightIntruders and AIObjectiveFindSafety would make the bot react to the threats,
|
||||
// but the reaction is delayed (and doesn't necessarily target this enemy), and in many cases the reaction would come only when the enemy attacks and triggers AIObjectiveCombat.
|
||||
AddCombatObjective(ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (AIObjectiveExtinguishFires.IsValidTarget(hull, Character))
|
||||
@@ -1076,14 +1059,14 @@ namespace Barotrauma
|
||||
if (AddTargets<AIObjectiveExtinguishFires, Hull>(Character, hull) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportfire"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
if (IsBallastFloraNoticeable(Character, hull) && newOrder == null)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportballastflora"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
if (!isFighting)
|
||||
@@ -1095,7 +1078,7 @@ namespace Barotrauma
|
||||
if (AddTargets<AIObjectiveFixLeaks, Gap>(Character, gap) && newOrder == null && !gap.IsRoomToRoom)
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["reportbreach"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
@@ -1110,7 +1093,7 @@ namespace Barotrauma
|
||||
if (AddTargets<AIObjectiveRescueAll, Character>(Character, target) && newOrder == null && (!Character.IsMedic || Character == target) && !ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
|
||||
{
|
||||
var orderPrefab = OrderPrefab.Prefabs["requestfirstaid"];
|
||||
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
|
||||
newOrder = new Order(orderPrefab, hull, targetItem: null, orderGiver: Character);
|
||||
targetHull = hull;
|
||||
}
|
||||
}
|
||||
@@ -1135,20 +1118,19 @@ namespace Barotrauma
|
||||
}
|
||||
if (newOrder != null && speak)
|
||||
{
|
||||
string msg = newOrder.GetChatMessage(string.Empty, targetHull?.DisplayName?.Value ?? string.Empty, givingOrderToSelf: false);
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
Character.Speak(msg, ChatMessageType.Default, identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), minDurationBetweenSimilar: 60f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
|
||||
Character.Speak(msg, messageType: ChatMessageType.Order);
|
||||
#if SERVER
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
|
||||
.WithTargetEntity(targetHull)
|
||||
.WithOrderGiver(Character), "", null, Character));
|
||||
.WithOrderGiver(Character), msg, targetCharacter: null, sender: Character));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1172,8 +1154,19 @@ namespace Barotrauma
|
||||
public static void ReportProblem(Character reporter, Order order, Hull targetHull = null)
|
||||
{
|
||||
if (reporter == null || order == null) { return; }
|
||||
var visibleHulls = targetHull is null ? new List<Hull>(reporter.GetVisibleHulls()) : new List<Hull> { targetHull };
|
||||
foreach (var hull in visibleHulls)
|
||||
if (targetHull == null)
|
||||
{
|
||||
foreach (var hull in reporter.GetVisibleHulls())
|
||||
{
|
||||
Report(hull);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Report(targetHull);
|
||||
}
|
||||
|
||||
void Report(Hull hull)
|
||||
{
|
||||
PropagateHullSafety(reporter, hull);
|
||||
RefreshTargets(reporter, order, hull);
|
||||
@@ -1353,7 +1346,7 @@ namespace Barotrauma
|
||||
// Inform other NPCs
|
||||
if (isAttackerInfected || cumulativeDamage > minorDamageThreshold || totalDamage > minorDamageThreshold)
|
||||
{
|
||||
if (GameMain.IsMultiplayer || !attacker.IsPlayer || Character.TeamID != attacker.TeamID)
|
||||
if (!attacker.IsPlayer || Character.TeamID != attacker.TeamID)
|
||||
{
|
||||
InformOtherNPCs(cumulativeDamage);
|
||||
}
|
||||
@@ -1421,7 +1414,12 @@ namespace Barotrauma
|
||||
if (otherCharacter.IsPlayer) { continue; }
|
||||
if (otherCharacter.AIController is not HumanAIController otherHumanAI) { continue; }
|
||||
if (!otherHumanAI.IsFriendly(Character)) { continue; }
|
||||
if (attacker.AIController is EnemyAIController enemyAI && otherHumanAI.IsFriendly(attacker))
|
||||
if (otherHumanAI.objectiveManager.IsCurrentObjective<AIObjectiveCombat>() || otherHumanAI.objectiveManager.HasActiveObjective<AIObjectiveCombat>())
|
||||
{
|
||||
// Already in combat, don't change target (because we are not attacked by the enemy)
|
||||
return;
|
||||
}
|
||||
if (attacker.AIController is EnemyAIController && otherHumanAI.IsFriendly(attacker))
|
||||
{
|
||||
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
|
||||
continue;
|
||||
@@ -1432,15 +1430,33 @@ namespace Barotrauma
|
||||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
|
||||
if (!isWitnessing)
|
||||
{
|
||||
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
|
||||
//don't react to the attack
|
||||
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID || !CheckReportRange(Character, otherCharacter, ReportRange))
|
||||
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
|
||||
{
|
||||
// Dead or in different team -> cannot report.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (otherHumanAI.objectiveManager.HasOrders())
|
||||
{
|
||||
// Unless witnessing the attack, don't react, if have been ordered to do something.
|
||||
// The combat objective would take a higher prio than the order.
|
||||
continue;
|
||||
}
|
||||
if (!CheckReportRange(Character, otherCharacter, ReportRange))
|
||||
{
|
||||
// Not inside report range -> cannot report.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
var combatMode = DetermineCombatMode(otherCharacter, cumulativeDamage, isWitnessing);
|
||||
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
|
||||
if (!isWitnessing)
|
||||
{
|
||||
if (combatMode is AIObjectiveCombat.CombatMode.Defensive or AIObjectiveCombat.CombatMode.Retreat)
|
||||
{
|
||||
// Ignore defensive and retreating behavior, unless witnessing the attack.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
|
||||
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
|
||||
}
|
||||
}
|
||||
@@ -1476,12 +1492,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (attacker.IsPlayer && c.TeamID == attacker.TeamID)
|
||||
{
|
||||
if (GameMain.IsSingleplayer || c.TeamID != attacker.TeamID)
|
||||
{
|
||||
// Bots in the player team never act aggressively in single player when attacked by the player
|
||||
// In multiplayer, they react only to players attacking them or other crew members
|
||||
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
// Bots in the player team never act aggressively when attacked by the player
|
||||
return Character == c && cumulativeDamage > minorDamageThreshold ? AIObjectiveCombat.CombatMode.Retreat : AIObjectiveCombat.CombatMode.None;
|
||||
}
|
||||
if (c.Submarine == null || !c.Submarine.GetConnectedSubs().Contains(attacker.Submarine))
|
||||
{
|
||||
@@ -1526,16 +1538,18 @@ namespace Barotrauma
|
||||
// Already targeting the attacker -> treat as a more serious threat.
|
||||
cumulativeDamage *= 2;
|
||||
currentCombatObjective.AllowHoldFire = false;
|
||||
c.IsCriminal = true;
|
||||
attacker.IsCriminal = true;
|
||||
attacker.IsActingOffensively = true;
|
||||
}
|
||||
if (c.IsCriminal)
|
||||
if (attacker.IsCriminal)
|
||||
{
|
||||
// Always react if the attacker has been misbehaving earlier.
|
||||
cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
|
||||
}
|
||||
if (cumulativeDamage > majorDamageThreshold)
|
||||
{
|
||||
c.IsCriminal = true;
|
||||
attacker.IsCriminal = true;
|
||||
attacker.IsActingOffensively = true;
|
||||
if (c.IsSecurity)
|
||||
{
|
||||
return AIObjectiveCombat.CombatMode.Offensive;
|
||||
@@ -1547,6 +1561,7 @@ namespace Barotrauma
|
||||
}
|
||||
else if (cumulativeDamage > minorDamageThreshold)
|
||||
{
|
||||
attacker.IsActingOffensively = true;
|
||||
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
|
||||
}
|
||||
else
|
||||
@@ -1611,7 +1626,6 @@ namespace Barotrauma
|
||||
{
|
||||
var objective = new AIObjectiveCombat(Character, target, mode, objectiveManager)
|
||||
{
|
||||
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
|
||||
AbortCondition = abortCondition,
|
||||
AllowHoldFire = allowHoldFire,
|
||||
SpeakWarnings = speakWarnings
|
||||
@@ -1718,8 +1732,9 @@ namespace Barotrauma
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool NeedsDivingGear(Hull hull, out bool needsSuit)
|
||||
|
||||
/// <param name="objectiveManager">Used for checking the objective.</param>
|
||||
public bool NeedsDivingGear(Hull hull, out bool needsSuit, AIObjectiveManager objectiveManager = null)
|
||||
{
|
||||
needsSuit = false;
|
||||
bool needsAir = Character.NeedsAir && Character.CharacterHealth.OxygenLowResistance < 1;
|
||||
@@ -1728,7 +1743,11 @@ namespace Barotrauma
|
||||
hull.LethalPressure > 0 ||
|
||||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
|
||||
{
|
||||
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
|
||||
if (!Character.IsImmuneToPressure)
|
||||
{
|
||||
// Always require a diving suit when operating an item in a flooding room.
|
||||
needsSuit = hull == null || hull.LethalPressure > 0 || objectiveManager?.CurrentOrder is AIObjectiveOperateItem operateItem && operateItem.GetTarget().Item.CurrentHull == hull;
|
||||
}
|
||||
return needsAir || needsSuit;
|
||||
}
|
||||
if (hull.WaterPercentage > 60 || (hull.IsWetRoom && hull.WaterPercentage > 10) || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
|
||||
@@ -1863,6 +1882,7 @@ namespace Barotrauma
|
||||
if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
|
||||
{
|
||||
character.IsCriminal = true;
|
||||
character.IsActingOffensively = true;
|
||||
}
|
||||
if (!TriggerSecurity(otherHumanAI, combatMode))
|
||||
{
|
||||
@@ -1903,6 +1923,11 @@ namespace Barotrauma
|
||||
public static void ItemTaken(Item item, Character thief)
|
||||
{
|
||||
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
|
||||
if (thief.IsBot && item.HasTag(AIObjectiveGetItem.AllowedItemsToTake))
|
||||
{
|
||||
// Bots are allowed to take diving gear or extinguishers, when they need them, without it being considered as stealing.
|
||||
return;
|
||||
}
|
||||
bool someoneSpoke = false;
|
||||
if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
|
||||
{
|
||||
@@ -2016,16 +2041,16 @@ namespace Barotrauma
|
||||
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
|
||||
/// There's now a cached value per each hull, which should prevent too frequent calculations.
|
||||
/// </summary>
|
||||
public static void PropagateHullSafety(Character character, Hull hull)
|
||||
private static void PropagateHullSafety(Character character, Hull hull)
|
||||
{
|
||||
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
|
||||
DoForEachBot(character, humanAi => humanAi.RefreshHullSafety(hull));
|
||||
}
|
||||
|
||||
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
|
||||
|
||||
private void RefreshHullSafety(Hull hull)
|
||||
{
|
||||
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
|
||||
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : null;
|
||||
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
|
||||
if (hullSafety > HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
@@ -2037,7 +2062,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static void RefreshTargets(Character character, Order order, Hull hull)
|
||||
private static void RefreshTargets(Character character, Order order, Hull hull)
|
||||
{
|
||||
switch (order.Identifier.Value.ToLowerInvariant())
|
||||
{
|
||||
@@ -2068,7 +2093,7 @@ namespace Barotrauma
|
||||
foreach (var enemy in Character.CharacterList)
|
||||
{
|
||||
if (enemy.CurrentHull != hull) { continue; }
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, false))
|
||||
if (AIObjectiveFightIntruders.IsValidTarget(enemy, character, targetCharactersInOtherSubs: false))
|
||||
{
|
||||
AddTargets<AIObjectiveFightIntruders, Character>(character, enemy);
|
||||
}
|
||||
@@ -2147,7 +2172,7 @@ namespace Barotrauma
|
||||
// Use the cached visible hulls
|
||||
visibleHulls = VisibleHulls;
|
||||
}
|
||||
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
|
||||
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires { Priority: > 0 } || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
|
||||
bool ignoreOxygen = HasDivingGear(character);
|
||||
bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
|
||||
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
|
||||
@@ -2157,12 +2182,26 @@ namespace Barotrauma
|
||||
}
|
||||
return safety;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns hull safety for the character without ignoring any threats.
|
||||
/// Useful for example, when we need to calculate a safety value of the hull regardless of the protective equipment or buffs of the character.
|
||||
/// No caching involved (always recalculated).
|
||||
/// </summary>
|
||||
public static float CalculateObjectiveHullSafety(Character character) => CalculateHullSafety(
|
||||
hull: character.CurrentHull,
|
||||
visibleHulls: character.AIController?.VisibleHulls ?? character.GetVisibleHulls(),
|
||||
character,
|
||||
ignoreEnemies: false, ignoreFire: false, ignoreWater: false, ignoreOxygen: false, ignorePressureProtection: true);
|
||||
|
||||
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
|
||||
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false, bool ignorePressureProtection = false)
|
||||
{
|
||||
bool isProtectedFromPressure = character.IsProtectedFromPressure;
|
||||
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
|
||||
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
|
||||
if (!ignorePressureProtection)
|
||||
{
|
||||
bool isProtectedFromPressure = character.IsProtectedFromPressure;
|
||||
if (hull == null) { return isProtectedFromPressure ? 100 : 0; }
|
||||
if (hull.LethalPressure > 0 && !isProtectedFromPressure) { return 0; }
|
||||
}
|
||||
// Oxygen factor should be 1 with 70% oxygen or more and 0.1 when the oxygen level is 30% or lower.
|
||||
// With insufficient oxygen, the safety of the hull should be 39, all the other factors aside. So, just below the HULL_SAFETY_THRESHOLD.
|
||||
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp((HULL_SAFETY_THRESHOLD - 1) / 100, 1, MathUtils.InverseLerp(HULL_LOW_OXYGEN_PERCENTAGE, 100 - HULL_LOW_OXYGEN_PERCENTAGE, hull.OxygenPercentage));
|
||||
@@ -2181,42 +2220,56 @@ namespace Barotrauma
|
||||
waterFactor = MathHelper.Lerp(1, HULL_SAFETY_THRESHOLD / 2 / 100, relativeWaterVolume);
|
||||
}
|
||||
}
|
||||
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
|
||||
if (!ignoreOxygen)
|
||||
{
|
||||
oxygenFactor = 1;
|
||||
}
|
||||
if (isProtectedFromPressure)
|
||||
{
|
||||
waterFactor = 1;
|
||||
if (!character.NeedsOxygen || character.CharacterHealth.OxygenLowResistance >= 1)
|
||||
{
|
||||
oxygenFactor = 1;
|
||||
}
|
||||
}
|
||||
float fireFactor = 1;
|
||||
if (!ignoreFire)
|
||||
{
|
||||
static float CalculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
|
||||
// Even the smallest fire reduces the safety by 50%
|
||||
float fire = visibleHulls?.Sum(CalculateFire) ?? CalculateFire(hull);
|
||||
float fire = CalculateFire(hull) + hull.linkedTo.Sum(e => CalculateFire(e as Hull));
|
||||
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
|
||||
|
||||
float CalculateFire(Hull h)
|
||||
{
|
||||
if (h is not Hull) { return 0; }
|
||||
bool isInDamageRange = h.FireSources.Any(fs => fs.IsInDamageRange(character, fs.DamageRange));
|
||||
if (isInDamageRange) { return 1; }
|
||||
// Even the smallest fire reduces the safety by 50%
|
||||
return h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
|
||||
}
|
||||
}
|
||||
float enemyFactor = 1;
|
||||
if (!ignoreEnemies)
|
||||
{
|
||||
int enemyCount = 0;
|
||||
float enemyCount = 0;
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
float countModifier = 1.0f;
|
||||
if (c.CurrentHull == null) { continue; }
|
||||
if (visibleHulls == null)
|
||||
{
|
||||
if (c.CurrentHull != hull) { continue; }
|
||||
if (c.CurrentHull != hull && !c.CurrentHull.linkedTo.Contains(hull)) { continue; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
|
||||
if (c.CurrentHull != hull && !c.CurrentHull.linkedTo.Contains(hull))
|
||||
{
|
||||
// Enemy in a visible room, but not in the same room -> a lower threat
|
||||
countModifier = 0.25f;
|
||||
}
|
||||
}
|
||||
if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
|
||||
{
|
||||
enemyCount++;
|
||||
enemyCount += countModifier;
|
||||
}
|
||||
}
|
||||
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
|
||||
// The hull safety decreases 90% per enemy up to 100%,
|
||||
// and 22.5% per each enemy in the visible, adjacent rooms
|
||||
enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
|
||||
}
|
||||
float dangerousItemsFactor = 1f;
|
||||
@@ -2236,7 +2289,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (hull == null)
|
||||
{
|
||||
return CalculateHullSafety(hull, character, visibleHulls);
|
||||
return CalculateHullSafety(null, character, visibleHulls);
|
||||
}
|
||||
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
|
||||
{
|
||||
@@ -2254,7 +2307,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (hull == null)
|
||||
{
|
||||
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
|
||||
return CalculateHullSafety(null, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
|
||||
}
|
||||
HullSafety hullSafety;
|
||||
if (character.AIController is HumanAIController controller)
|
||||
@@ -2279,11 +2332,14 @@ namespace Barotrauma
|
||||
return hullSafety.safety;
|
||||
}
|
||||
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
|
||||
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false, bool ignoreHuskDisguising = false)
|
||||
{
|
||||
if (other.IsHusk && !onlySameTeam)
|
||||
if (onlySameTeam)
|
||||
{
|
||||
ignoreHuskDisguising = true;
|
||||
}
|
||||
if (other.IsHusk && !ignoreHuskDisguising)
|
||||
{
|
||||
// Disguised as husk
|
||||
return me.IsDisguisedAsHusk;
|
||||
}
|
||||
else
|
||||
@@ -2435,7 +2491,7 @@ namespace Barotrauma
|
||||
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
|
||||
{
|
||||
if (character == null) { return; }
|
||||
foreach (var c in Character.CharacterList)
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
|
||||
{
|
||||
|
||||
@@ -119,10 +119,13 @@ namespace Barotrauma
|
||||
steering += base.DoSteeringSeek(targetSimPos, weight);
|
||||
}
|
||||
|
||||
public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="outsideNodePenalty">Additional cost applied to outside nodes. When the character is inside an extra cost of 100 is also automatically added for outside nodes, unless the character is protected from the pressure. Used for example to prevent monsters from preferring outside nodes when they already are inside.</param>
|
||||
public void SteeringSeek(Vector2 target, float weight, float minGapWidth = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true, float outsideNodePenalty = 0)
|
||||
{
|
||||
// Have to use a variable here or resetting doesn't work.
|
||||
Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
|
||||
Vector2 addition = CalculateSteeringSeek(target, weight, minGapWidth, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity, outsideNodePenalty);
|
||||
steering += addition;
|
||||
}
|
||||
|
||||
@@ -139,9 +142,9 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
|
||||
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true, float outsideNodePenalty = 0)
|
||||
{
|
||||
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
|
||||
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.IsAtEndNode || currentPath.CurrentNode == null;
|
||||
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
|
||||
{
|
||||
// If the target has moved, we need a new path.
|
||||
@@ -182,7 +185,7 @@ namespace Barotrauma
|
||||
Vector2 currentPos = host.SimPosition;
|
||||
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
|
||||
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
|
||||
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
|
||||
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility, outsideNodePenalty);
|
||||
bool useNewPath = needsNewPath;
|
||||
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
|
||||
{
|
||||
@@ -792,7 +795,7 @@ namespace Barotrauma
|
||||
float? penalty = GetSingleNodePenalty(nextNode);
|
||||
if (penalty == null) { return null; }
|
||||
bool nextNodeAboveWaterLevel = nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y;
|
||||
if (!character.CanClimb)
|
||||
if (!character.CanClimb && node.Waypoint.Stairs == null && nextNode.Waypoint.Stairs == null)
|
||||
{
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (!nextNode.Waypoint.Ladders.Item.IsInteractable(character) || character.LockHands) ||
|
||||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
|
||||
@@ -888,6 +891,7 @@ namespace Barotrauma
|
||||
return penalty;
|
||||
}
|
||||
|
||||
// TODO: Long and complex. Consider refactoring.
|
||||
public static float smallRoomSize = 500;
|
||||
public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
|
||||
{
|
||||
@@ -915,36 +919,92 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
float leftDist = character.Position.X - currentHull.Rect.X;
|
||||
float rightDist = currentHull.Rect.Right - character.Position.X;
|
||||
if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
|
||||
bool isVerySmallRoom = roomWidth < smallRoomSize;
|
||||
Hull nextRoom = null;
|
||||
if (!stayStillInTightSpace && isVerySmallRoom)
|
||||
{
|
||||
if (Math.Abs(rightDist - leftDist) > wallAvoidDistance / 2)
|
||||
float closestDistance = 0;
|
||||
// Try to steer to the next room
|
||||
foreach (Gap gap in currentHull.ConnectedGaps)
|
||||
{
|
||||
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(rightDist - leftDist));
|
||||
return;
|
||||
}
|
||||
else if (stayStillInTightSpace)
|
||||
{
|
||||
Reset();
|
||||
return;
|
||||
if (gap.Open < 1.0f) { continue; }
|
||||
float feetPos = ConvertUnits.ToDisplayUnits(character.AnimController.FloorY);
|
||||
float gapTop = gap.Rect.Y;
|
||||
float gapBottom = gap.Rect.Y - gap.Rect.Height;
|
||||
const float margin = 25;
|
||||
if (character.Position.Y > gapTop || feetPos < gapBottom - margin)
|
||||
{
|
||||
// If the character is above or below the gap, they can't walk through it.
|
||||
continue;
|
||||
}
|
||||
Hull room = null;
|
||||
foreach (var entity in gap.linkedTo)
|
||||
{
|
||||
if (entity is not Hull hull) { continue; }
|
||||
if (hull.Submarine != character.Submarine) { continue; }
|
||||
if (hull.Rect.Width < smallRoomSize) { continue; }
|
||||
if (hull != currentHull)
|
||||
{
|
||||
room = hull;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (room == null) { continue; }
|
||||
Vector2 toGap = gap.Position - character.Position;
|
||||
float distance = Math.Abs(toGap.X);
|
||||
if (nextRoom == null || distance < closestDistance)
|
||||
{
|
||||
closestDistance = distance;
|
||||
nextRoom = room;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (leftDist < wallAvoidDistance)
|
||||
if (nextRoom != null)
|
||||
{
|
||||
float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
|
||||
SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
|
||||
float toNextRoom = nextRoom.Position.X - character.Position.X;
|
||||
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(toNextRoom));
|
||||
WanderAngle = 0.0f;
|
||||
}
|
||||
else if (rightDist < wallAvoidDistance)
|
||||
{
|
||||
float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
|
||||
SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
|
||||
WanderAngle = MathHelper.Pi;
|
||||
}
|
||||
else
|
||||
{
|
||||
wander = true;
|
||||
if (!stayStillInTightSpace && isVerySmallRoom)
|
||||
{
|
||||
// Reset regardless, because moving inside the room would look glitchy.
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
float leftDist = character.Position.X - currentHull.Rect.X;
|
||||
float rightDist = currentHull.Rect.Right - character.Position.X;
|
||||
if (leftDist < wallAvoidDistance && rightDist < wallAvoidDistance)
|
||||
{
|
||||
float diff = rightDist - leftDist;
|
||||
if (Math.Abs(diff) > wallAvoidDistance / 2)
|
||||
{
|
||||
SteeringManual(deltaTime, Vector2.UnitX * Math.Sign(diff));
|
||||
return;
|
||||
}
|
||||
else if (stayStillInTightSpace)
|
||||
{
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (leftDist < wallAvoidDistance)
|
||||
{
|
||||
float speed = (wallAvoidDistance - leftDist) / wallAvoidDistance;
|
||||
SteeringManual(deltaTime, Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
|
||||
WanderAngle = 0.0f;
|
||||
}
|
||||
else if (rightDist < wallAvoidDistance)
|
||||
{
|
||||
float speed = (wallAvoidDistance - rightDist) / wallAvoidDistance;
|
||||
SteeringManual(deltaTime, -Vector2.UnitX * MathHelper.Clamp(speed, 0.25f, 1));
|
||||
WanderAngle = MathHelper.Pi;
|
||||
}
|
||||
else
|
||||
{
|
||||
wander = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ namespace Barotrauma
|
||||
public bool IsIgnoredAtOutpost()
|
||||
{
|
||||
if (!IgnoreAtOutpost) { return false; }
|
||||
if (!Level.IsLoadedFriendlyOutpost) { return false; }
|
||||
if (!Level.IsLoadedFriendlyOutpost && GameMain.GameSession.GameMode is not TestGameMode) { return false; }
|
||||
if (!character.IsOnPlayerTeam || character.IsFriendlyNPCTurnedHostile) { return false; }
|
||||
if (character.Submarine?.Info == null) { return false; }
|
||||
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
|
||||
|
||||
+37
-17
@@ -48,19 +48,22 @@ namespace Barotrauma
|
||||
protected override bool CheckObjectiveState() => false;
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
// Target is climbing -> stop following the objective (soft abandon, without ignoring the target).
|
||||
Priority = 0;
|
||||
}
|
||||
else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
{
|
||||
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
|
||||
{
|
||||
Priority = objectiveManager.GetOrderPriority(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Priority = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
@@ -86,18 +89,18 @@ namespace Barotrauma
|
||||
onCompleted: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
if (character.IsClimbing)
|
||||
if (character.IsClimbing || HumanAIController.CurrentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD || HumanAIController.CalculateObjectiveHullSafety(Target) < HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0,
|
||||
// but if this still happens, we'll have to abandon the objective
|
||||
// because it's not currently possible to hold to characters and ladders at the same time.
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
// Shouldn't start inspecting characters when they climb, but we can still get here, if they start climbing while we are moving at them.
|
||||
// If that happens, let's abandon the objective, because it's not currently possible to hold to characters and ladders at the same time.
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentState = State.Inspect;
|
||||
stolenItems.Clear();
|
||||
Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems);
|
||||
Target.Inventory.FindAllItems(it => IsItemIllegitimate(Target, it), recursive: true, stolenItems);
|
||||
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value);
|
||||
}
|
||||
},
|
||||
@@ -190,11 +193,23 @@ namespace Barotrauma
|
||||
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target);
|
||||
if (stolenItemsOnCharacter.Any())
|
||||
{
|
||||
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
|
||||
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
|
||||
foreach (var stolenItem in stolenItemsOnCharacter)
|
||||
if (Target.IsBot)
|
||||
{
|
||||
HumanAIController.ApplyStealingReputationLoss(stolenItem);
|
||||
// Bots automatically comply and drop stolen items when being inspected.
|
||||
foreach (Item item in stolenItemsOnCharacter)
|
||||
{
|
||||
item.Drop(Target);
|
||||
}
|
||||
character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
|
||||
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
|
||||
foreach (var stolenItem in stolenItemsOnCharacter)
|
||||
{
|
||||
HumanAIController.ApplyStealingReputationLoss(stolenItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -242,5 +257,10 @@ namespace Barotrauma
|
||||
currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay;
|
||||
warnTimer = currentWarnDelay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks for illegitimate item, ignoring handcuffs equipped on the owner.
|
||||
/// </summary>
|
||||
public static bool IsItemIllegitimate(Character owner, Item item) => item.Illegitimate && (!item.HasTag(Tags.HandLockerItem) || !owner.HasEquippedItem(item));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -18,11 +18,11 @@ namespace Barotrauma
|
||||
public bool IsPriority { get; set; }
|
||||
|
||||
private readonly List<Item> ignoredContainers = new List<Item>();
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private int itemIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
|
||||
/// Allows <see cref="moveItemObjective"/> to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
|
||||
/// </summary>
|
||||
protected override bool ConcurrentObjectives => true;
|
||||
|
||||
@@ -53,9 +53,9 @@ namespace Barotrauma
|
||||
float reduction = IsPriority ? 1 : isSelected ? 2 : 3;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - reduction;
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
|
||||
if (decontainObjective == null)
|
||||
if (moveItemObjective == null)
|
||||
{
|
||||
// Halve the priority until there's a decontain objective (a valid container was found).
|
||||
// Halve the priority until there's a moveItemObjective (a valid container was found).
|
||||
Priority /= 2;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ namespace Barotrauma
|
||||
s == InvSlotType.OuterClothes ||
|
||||
s == InvSlotType.HealthInterface);
|
||||
|
||||
TryAddSubObjective(ref decontainObjective, () => new AIObjectiveDecontainItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
TryAddSubObjective(ref moveItemObjective, () => new AIObjectiveMoveItem(character, item, objectiveManager, targetContainer: suitableContainer.GetComponent<ItemContainer>())
|
||||
{
|
||||
Equip = equip,
|
||||
TakeWholeStack = true,
|
||||
@@ -99,7 +99,7 @@ namespace Barotrauma
|
||||
{
|
||||
HumanAIController.ReequipUnequipped();
|
||||
}
|
||||
if (decontainObjective != null && decontainObjective.ContainObjective != null && decontainObjective.ContainObjective.CanBeCompleted)
|
||||
if (moveItemObjective is { ContainObjective.CanBeCompleted: true })
|
||||
{
|
||||
ignoredContainers.Add(suitableContainer);
|
||||
}
|
||||
@@ -144,7 +144,7 @@ namespace Barotrauma
|
||||
base.Reset();
|
||||
ignoredContainers.Clear();
|
||||
itemIndex = 0;
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
}
|
||||
|
||||
public void DropTarget()
|
||||
|
||||
+238
-84
@@ -4,15 +4,18 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using static Barotrauma.AIObjectiveFindSafety;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using FarseerPhysics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveCombat : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "combat".ToIdentifier();
|
||||
|
||||
public override string DebugTag => $"{Identifier} ({Mode})";
|
||||
|
||||
public override bool KeepDivingGearOn => true;
|
||||
public override bool IgnoreUnsafeHulls => true;
|
||||
@@ -35,10 +38,9 @@ namespace Barotrauma
|
||||
private bool allowCooldown;
|
||||
|
||||
public Character Enemy { get; private set; }
|
||||
public bool HoldPosition { get; set; }
|
||||
|
||||
|
||||
private Item _weapon;
|
||||
private Item Weapon
|
||||
public Item Weapon
|
||||
{
|
||||
get { return _weapon; }
|
||||
set
|
||||
@@ -48,6 +50,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
private ItemComponent _weaponComponent;
|
||||
private bool hasValidRangedWeapon;
|
||||
private ItemComponent WeaponComponent
|
||||
{
|
||||
get
|
||||
@@ -87,7 +90,9 @@ namespace Barotrauma
|
||||
private const float DistanceCheckInterval = 0.2f;
|
||||
private float distanceTimer;
|
||||
|
||||
private const float CloseDistanceThreshold = 300;
|
||||
private const float CloseDistance = 300;
|
||||
private const float MeleeDistance = 125;
|
||||
private const float TooCloseToShoot = 100;
|
||||
private const float FloorHeightApproximate = 100;
|
||||
|
||||
public bool AllowHoldFire;
|
||||
@@ -168,13 +173,7 @@ namespace Barotrauma
|
||||
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
if (mode == CombatMode.None)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError("Combat mode == None");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
Debug.Assert(mode != CombatMode.None);
|
||||
Enemy = enemy;
|
||||
coolDownTimer = coolDown;
|
||||
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
|
||||
@@ -229,6 +228,7 @@ namespace Barotrauma
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
isAimBlocked = false;
|
||||
ignoreWeaponTimer -= deltaTime;
|
||||
checkWeaponsTimer -= deltaTime;
|
||||
if (reloadTimer > 0)
|
||||
@@ -331,68 +331,155 @@ namespace Barotrauma
|
||||
pathBackTimer -= deltaTime;
|
||||
}
|
||||
}
|
||||
if (standUpTimer > 0)
|
||||
{
|
||||
standUpTimer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Crouch by default so that others can shoot from behind. Disabled when the line of sight is blocked and while moving.
|
||||
allowCrouching = true;
|
||||
}
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
BlockedPositions ??= new List<Vector2>();
|
||||
BlockedPositions.Clear();
|
||||
}
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
if (Mode != CombatMode.Retreat && TryArm())
|
||||
{
|
||||
OperateWeapon(deltaTime);
|
||||
}
|
||||
if (HoldPosition)
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
isMoving = false;
|
||||
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
|
||||
{
|
||||
Move(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool isMoving;
|
||||
private void Move(float deltaTime)
|
||||
{
|
||||
switch (Mode)
|
||||
if (Mode == CombatMode.Retreat)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
case CombatMode.Arrest:
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
else if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.CurrentOrder is AIObjectiveGoTo gotoObjective)
|
||||
{
|
||||
if (gotoObjective.IsWaitOrder && WeaponComponent is MeleeWeapon && IsEnemyClose(CloseDistance))
|
||||
{
|
||||
// Ordered to wait near the enemy with a melee weapon -> engage.
|
||||
Engage(deltaTime);
|
||||
break;
|
||||
case CombatMode.Defensive:
|
||||
if (character.IsOnPlayerTeam && !Enemy.IsPlayer && objectiveManager.IsCurrentOrder<AIObjectiveGoTo>())
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ordered to follow -> keep following.
|
||||
if (!gotoObjective.IsCloseEnough)
|
||||
{
|
||||
if ((character.CurrentHull == null || character.CurrentHull == Enemy.CurrentHull) && sqrDistance < 200 * 200)
|
||||
isMoving = true;
|
||||
}
|
||||
gotoObjective.FaceTargetOnCompleted = false;
|
||||
gotoObjective.ForceAct(deltaTime);
|
||||
if (!character.AnimController.InWater && IsEnemyClose(CloseDistance))
|
||||
{
|
||||
// If close to the enemy, face it, so that we can attack it.
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
if (!gotoObjective.ShouldRun(true))
|
||||
{
|
||||
Engage(deltaTime);
|
||||
ForceWalk = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Defensive:
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
case CombatMode.Offensive when hasValidRangedWeapon && IsEnemyClose(CloseDistance):
|
||||
// Too close to the enemy -> try to back off.
|
||||
Hull currentHull = character.CurrentHull;
|
||||
bool backOff = currentHull != null;
|
||||
Vector2 escapeVel = Vector2.Zero;
|
||||
if (backOff)
|
||||
{
|
||||
int previousEnemyDir = 0;
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
|
||||
if (enemy.CurrentHull == null) { continue; }
|
||||
if (currentHull != enemy.CurrentHull && !currentHull.linkedTo.Contains(enemy.CurrentHull)) { continue; }
|
||||
Vector2 dir = character.Position - enemy.Position;
|
||||
int enemyDir = Math.Sign(dir.X);
|
||||
if (enemyDir == 0)
|
||||
{
|
||||
// Exactly at the same pos.
|
||||
if (previousEnemyDir != 0)
|
||||
{
|
||||
// Another enemy at either side -> Ignore this enemy.
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just choose either direction.
|
||||
enemyDir = Rand.Value() > 0.5f ? 1 : -1;
|
||||
}
|
||||
}
|
||||
if (previousEnemyDir != 0 && enemyDir != previousEnemyDir)
|
||||
{
|
||||
// Don't back off when there are enemies in different directions, because that's doomed.
|
||||
backOff = false;
|
||||
break;
|
||||
}
|
||||
previousEnemyDir = enemyDir;
|
||||
// This formula is slightly modified from AIObjectiveFindSafety.UpdateSimpleEscape().
|
||||
float distMultiplier = MathHelper.Clamp(MeleeDistance / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
|
||||
escapeVel += new Vector2(enemyDir * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
|
||||
}
|
||||
if (escapeVel == Vector2.Zero)
|
||||
{
|
||||
backOff = false;
|
||||
}
|
||||
if (backOff)
|
||||
{
|
||||
// Only move if we haven't reached the edge of the room.
|
||||
float left = currentHull.Rect.X + 50;
|
||||
float right = currentHull.Rect.Right - 50;
|
||||
backOff = escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right;
|
||||
}
|
||||
}
|
||||
if (backOff)
|
||||
{
|
||||
BackOff();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep following the goto target
|
||||
var gotoObjective = objectiveManager.GetOrder<AIObjectiveGoTo>();
|
||||
if (gotoObjective != null)
|
||||
{
|
||||
gotoObjective.ForceAct(deltaTime);
|
||||
if (!character.AnimController.InWater)
|
||||
{
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
ForceWalk = true;
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
}
|
||||
Engage(deltaTime);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Retreat:
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
|
||||
void BackOff()
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
isMoving = true;
|
||||
if (!IsEnemyClose(MeleeDistance))
|
||||
{
|
||||
ForceWalk = true;
|
||||
}
|
||||
HumanAIController.FaceTarget(Enemy);
|
||||
HumanAIController.AutoFaceMovement = false;
|
||||
character.ReleaseSecondaryItem();
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Engage(deltaTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,7 +494,8 @@ namespace Barotrauma
|
||||
bool isAllowedToSeekWeapons = character.IsHostileEscortee || character.IsPrisoner || // Prisoners and terrorists etc are always allowed to seek new weapons.
|
||||
(character.IsInFriendlySub // Other characters need to be on a friendly sub in order to "know" where the weapons are. This also prevents NPCs "stealing" player items.
|
||||
&& IsOffensiveOrArrest // = Defensive or retreating AI shouldn't seek new weapons.
|
||||
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
|
||||
&& !character.IsInstigator // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
|
||||
&& objectiveManager.CurrentOrder is not AIObjectiveGoTo); // if ordered to wait/follow, shouldn't go seeking new weapons.
|
||||
if (checkWeaponsTimer < 0)
|
||||
{
|
||||
checkWeaponsTimer = CheckWeaponsInterval;
|
||||
@@ -433,7 +521,12 @@ namespace Barotrauma
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
}
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
|
||||
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null;
|
||||
if (seekAmmo)
|
||||
{
|
||||
// Bots set to arrest the target are always allowed to seek (or spawn) more ammo, because otherwise they might not be able to stun the target and need to use lethal weapons.
|
||||
seekAmmo = Mode == CombatMode.Arrest || !IsEnemyClose(CloseDistance);
|
||||
}
|
||||
if (Reload(seekAmmo: seekAmmo))
|
||||
{
|
||||
// All good, we can use the weapon.
|
||||
@@ -479,7 +572,7 @@ namespace Barotrauma
|
||||
Mode = CombatMode.Retreat;
|
||||
}
|
||||
}
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
|
||||
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistance))))
|
||||
{
|
||||
// No weapon or only a poor weapon equipped -> try to find better.
|
||||
RemoveSubObjective(ref retreatObjective);
|
||||
@@ -488,7 +581,7 @@ namespace Barotrauma
|
||||
constructor: () => new AIObjectiveGetItem(character, "weapon".ToIdentifier(), objectiveManager, equip: true, checkInventory: false)
|
||||
{
|
||||
AllowStealing = HumanAIController.IsMentallyUnstable,
|
||||
AbortCondition = obj => IsEnemyClose(200),
|
||||
AbortCondition = _ => IsEnemyClose(CloseDistance / 2),
|
||||
EvaluateCombatPriority = false, // Use a custom formula instead
|
||||
GetItemPriority = i =>
|
||||
{
|
||||
@@ -581,6 +674,12 @@ namespace Barotrauma
|
||||
|
||||
private void OperateWeapon(float deltaTime)
|
||||
{
|
||||
if (isMoving && character.IsClimbing)
|
||||
{
|
||||
// Don't climb and shoot at the same time, because it messes up the aiming.
|
||||
ClearInputs();
|
||||
return;
|
||||
}
|
||||
switch (Mode)
|
||||
{
|
||||
case CombatMode.Offensive:
|
||||
@@ -789,17 +888,22 @@ namespace Barotrauma
|
||||
return containers.None() || containers.Any(container =>
|
||||
(container as ItemContainer)?.Inventory.AllItems.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
|
||||
}
|
||||
|
||||
|
||||
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
|
||||
{
|
||||
hasValidRangedWeapon = false;
|
||||
weaponComponent = null;
|
||||
float bestPriority = 0;
|
||||
float lethalDmg = -1;
|
||||
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
|
||||
foreach (var weapon in weaponList)
|
||||
bool prioritizeMelee = IsEnemyClose(TooCloseToShoot) || EnemyAIController.IsLatchedTo(Enemy, character);
|
||||
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistance);
|
||||
foreach (ItemComponent weapon in weaponList)
|
||||
{
|
||||
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
|
||||
if (priority >= GoodWeaponPriority && weapon is RangedWeapon or RepairTool)
|
||||
{
|
||||
hasValidRangedWeapon = true;
|
||||
}
|
||||
if (priority > bestPriority)
|
||||
{
|
||||
weaponComponent = weapon;
|
||||
@@ -947,6 +1051,7 @@ namespace Barotrauma
|
||||
|
||||
private void Retreat(float deltaTime)
|
||||
{
|
||||
isMoving = true;
|
||||
if (!Enemy.IsHuman && !character.IsInFriendlySub)
|
||||
{
|
||||
// Only relevant when we are retreating from monsters and are not inside a friendly sub.
|
||||
@@ -1044,6 +1149,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
|
||||
{
|
||||
isMoving = true;
|
||||
character.ReleaseSecondaryItem();
|
||||
// Swim towards the target
|
||||
SteeringManager.Reset();
|
||||
@@ -1094,7 +1200,7 @@ namespace Barotrauma
|
||||
}
|
||||
});
|
||||
if (followTargetObjective == null) { return; }
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
|
||||
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDownOrRagdolled && !arrestingRegistered)
|
||||
{
|
||||
bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
|
||||
if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
@@ -1119,12 +1225,20 @@ namespace Barotrauma
|
||||
followTargetObjective.CloseEnough =
|
||||
WeaponComponent switch
|
||||
{
|
||||
RangedWeapon => 1000,
|
||||
RangedWeapon => isAimBlocked ? BlockedDistance : 1000,
|
||||
MeleeWeapon mw => mw.Range,
|
||||
RepairTool rt => rt.Range,
|
||||
_ => 50
|
||||
};
|
||||
}
|
||||
if (isAimBlocked)
|
||||
{
|
||||
ForceWalk = true;
|
||||
}
|
||||
if (!followTargetObjective.IsCloseEnough)
|
||||
{
|
||||
isMoving = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveFollowTarget()
|
||||
@@ -1142,19 +1256,23 @@ namespace Barotrauma
|
||||
|
||||
private void OnArrestTargetReached()
|
||||
{
|
||||
if (!Enemy.IsKnockedDown)
|
||||
if (!Enemy.IsKnockedDownOrRagdolled)
|
||||
{
|
||||
RemoveFollowTarget();
|
||||
return;
|
||||
}
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
// Confiscate stolen goods and all weapons
|
||||
foreach (var item in Enemy.Inventory.AllItemsMod)
|
||||
{
|
||||
// Ignore handcuffs already on the target.
|
||||
if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
|
||||
if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
|
||||
AIObjectiveFindThieves.MarkTargetAsInspected(character);
|
||||
bool confiscateItem = AIObjectiveCheckStolenItems.IsItemIllegitimate(Enemy, item);
|
||||
if (!confiscateItem && Enemy.IsActingOffensively)
|
||||
{
|
||||
// Confiscate any weapons or items that can be used offensively.
|
||||
confiscateItem = item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 };
|
||||
}
|
||||
if (confiscateItem)
|
||||
{
|
||||
item.Drop(character);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
|
||||
@@ -1169,7 +1287,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
if (matchingItems.Any() &&
|
||||
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
|
||||
!Enemy.IsUnconscious && Enemy.IsKnockedDownOrRagdolled && character.CanInteractWith(Enemy) && !Enemy.LockHands)
|
||||
{
|
||||
var handCuffs = matchingItems.First();
|
||||
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
|
||||
@@ -1202,7 +1320,8 @@ namespace Barotrauma
|
||||
RemoveFollowTarget();
|
||||
var itemContainer = Weapon.GetComponent<ItemContainer>();
|
||||
TryAddSubObjective(ref seekAmmunitionObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager)
|
||||
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, itemContainer, objectiveManager,
|
||||
spawnItemIfNotFound: !character.IsOnPlayerTeam && character.AIController.HasInfiniteItemSpawns(ammunitionIdentifiers))
|
||||
{
|
||||
ItemCount = itemContainer.MainContainerCapacity * itemContainer.MaxStackSize,
|
||||
checkInventory = false,
|
||||
@@ -1224,10 +1343,9 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private bool Reload(bool seekAmmo)
|
||||
{
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (WeaponComponent == null) { return false; }
|
||||
if (Weapon.OwnInventory == null) { return true; }
|
||||
// Eject empty ammo
|
||||
HumanAIController.UnequipEmptyItems(Weapon);
|
||||
HumanAIController.UnequipEmptyItems(Weapon, allowDestroying: !character.IsOnPlayerTeam);
|
||||
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
|
||||
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
|
||||
{
|
||||
@@ -1267,7 +1385,7 @@ namespace Barotrauma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
else if (IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
{
|
||||
// Inventory not drawn = it's not interactable
|
||||
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
|
||||
@@ -1277,6 +1395,20 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool isAimBlocked;
|
||||
private float _blockedDistance;
|
||||
private float BlockedDistance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_blockedDistance <= 0)
|
||||
{
|
||||
_blockedDistance = CloseDistance * Rand.Range(1.0f, 1.3f);
|
||||
}
|
||||
return _blockedDistance;
|
||||
}
|
||||
}
|
||||
public List<Vector2> BlockedPositions;
|
||||
private void Attack(float deltaTime)
|
||||
{
|
||||
character.CursorPosition = Enemy.WorldPosition;
|
||||
@@ -1378,28 +1510,49 @@ namespace Barotrauma
|
||||
var pickedBodies = Submarine.PickBodies(Weapon.SimPosition, Submarine.GetRelativeSimPosition(from: Weapon, to: Enemy),
|
||||
ignoredBodies: character.AnimController.LimbBodies,
|
||||
Physics.CollisionCharacter);
|
||||
|
||||
foreach (var body in pickedBodies)
|
||||
{
|
||||
Character target = body.UserData switch
|
||||
if (body.UserData is Limb limb)
|
||||
{
|
||||
Character c => c,
|
||||
Limb limb => limb.character,
|
||||
_ => null
|
||||
};
|
||||
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
|
||||
{
|
||||
return;
|
||||
Character target = limb.character;
|
||||
if (target != null && target != Enemy && HumanAIController.IsFriendly(target))
|
||||
{
|
||||
// Blocked by a friendly target.
|
||||
isAimBlocked = true;
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
BlockedPositions.Add(ConvertUnits.ToDisplayUnits(body.Position));
|
||||
}
|
||||
// Stand up, so that we might shoot past the friendlies that are crouching.
|
||||
allowCrouching = false;
|
||||
standUpTimer = StandUpCooldown;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
UseWeapon(deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool allowCrouching;
|
||||
private float standUpTimer;
|
||||
private const float StandUpCooldown = 5;
|
||||
private void UseWeapon(float deltaTime)
|
||||
{
|
||||
// Never allow friendly crew (bots) to attack with deadly weapons.
|
||||
if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
|
||||
// Enable this to debug intentional friendly fire.
|
||||
// if (isLethalWeapon && character.TeamID == Enemy.TeamID && character.IsOnPlayerTeam)
|
||||
// {
|
||||
// // Never allow friendly crew (bots) to attack with deadly weapons (this check should be redundant)
|
||||
// Debugger.Break();
|
||||
// return;
|
||||
// }
|
||||
if (allowCrouching && !isMoving && !character.AnimController.InWater && WeaponComponent is not MeleeWeapon)
|
||||
{
|
||||
HumanAIController.AnimController.Crouch();
|
||||
}
|
||||
character.SetInput(InputType.Shoot, hit: false, held: true);
|
||||
Weapon.Use(deltaTime, user: character);
|
||||
SetReloadTime(WeaponComponent);
|
||||
@@ -1512,6 +1665,7 @@ namespace Barotrauma
|
||||
hasAimed = false;
|
||||
holdFireTimer = 0;
|
||||
pathBackTimer = 0;
|
||||
standUpTimer = 0;
|
||||
isLethalWeapon = false;
|
||||
canSeeTarget = false;
|
||||
seekWeaponObjective = null;
|
||||
|
||||
+8
-3
@@ -48,6 +48,8 @@ namespace Barotrauma
|
||||
public int? RemoveMax { get; set; }
|
||||
|
||||
public bool MoveWholeStack { get; set; }
|
||||
|
||||
public bool AllowStealing { get; set; }
|
||||
|
||||
private int _itemCount = 1;
|
||||
public int ItemCount
|
||||
@@ -147,11 +149,11 @@ namespace Barotrauma
|
||||
|
||||
if (RemoveExisting || (RemoveExistingWhenNecessary && !CanBePut(container.Inventory, TargetSlot, ItemToContain)))
|
||||
{
|
||||
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax);
|
||||
HumanAIController.UnequipContainedItems(container.Item, predicate: RemoveExistingPredicate, unequipMax: RemoveMax, allowDestroying: spawnItemIfNotFound);
|
||||
}
|
||||
else if (RemoveEmpty)
|
||||
{
|
||||
HumanAIController.UnequipEmptyItems(container.Item);
|
||||
HumanAIController.UnequipEmptyItems(container.Item, allowDestroying: spawnItemIfNotFound);
|
||||
}
|
||||
Inventory originalInventory = ItemToContain.ParentInventory;
|
||||
var slots = originalInventory?.FindIndices(ItemToContain);
|
||||
@@ -195,6 +197,7 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(container.Item, character, objectiveManager, getDivingGearIfNeeded: AllowToFindDivingGear)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = container.Item.Name,
|
||||
AbortCondition = obj =>
|
||||
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
|
||||
@@ -231,7 +234,9 @@ namespace Barotrauma
|
||||
return (RemoveEmpty ? container.CanBeContained(potentialItem) : container.Inventory.CanBePut(potentialItem)) && container.ShouldBeContained(potentialItem, out _);
|
||||
},
|
||||
ItemCount = ItemCount,
|
||||
TakeWholeStack = MoveWholeStack
|
||||
TakeWholeStack = MoveWholeStack,
|
||||
ContainTarget = container,
|
||||
AllowStealing = AllowStealing
|
||||
}, onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
+5
-5
@@ -14,7 +14,7 @@ namespace Barotrauma
|
||||
|
||||
private Deconstructor deconstructor;
|
||||
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
public AIObjectiveDeconstructItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
|
||||
@@ -37,8 +37,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
TryAddSubObjective(ref decontainObjective,
|
||||
constructor: () => new AIObjectiveDecontainItem(character, Item, objectiveManager,
|
||||
TryAddSubObjective(ref moveItemObjective,
|
||||
constructor: () => new AIObjectiveMoveItem(character, Item, objectiveManager,
|
||||
sourceContainer: Item.Container?.GetComponent<ItemContainer>(), targetContainer: deconstructor.InputContainer, priorityModifier: PriorityModifier)
|
||||
{
|
||||
Equip = true,
|
||||
@@ -64,7 +64,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
});
|
||||
}
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
RemoveSubObjective(ref moveItemObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -125,7 +125,7 @@ namespace Barotrauma
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
}
|
||||
|
||||
public void DropTarget()
|
||||
|
||||
+39
-38
@@ -39,40 +39,39 @@ namespace Barotrauma
|
||||
// Don't go into rooms with any enemies, unless it's an order
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
else
|
||||
// Prioritize fires that currently damage the character.
|
||||
bool inDamageRange = targetHull.FireSources.Any(fs => fs.IsInDamageRange(character, fs.DamageRange));
|
||||
float severity = inDamageRange ? 1.0f : AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
float distanceFactor = targetHull == character.CurrentHull ? 1.0f
|
||||
: HumanAIController.VisibleHulls.Contains(targetHull) ? 0.75f : 0.0f;
|
||||
|
||||
if (distanceFactor <= 0.0f)
|
||||
{
|
||||
float characterY = character.CurrentHull?.WorldPosition.Y ?? character.WorldPosition.Y;
|
||||
|
||||
float distanceFactor = 1.0f;
|
||||
if (targetHull != character.CurrentHull &&
|
||||
!HumanAIController.VisibleHulls.Contains(targetHull))
|
||||
{
|
||||
distanceFactor =
|
||||
GetDistanceFactor(
|
||||
new Vector2(character.WorldPosition.Y, characterY),
|
||||
targetHull.WorldPosition,
|
||||
verticalDistanceMultiplier: 3,
|
||||
maxDistance: 5000,
|
||||
factorAtMaxDistance: 0.1f);
|
||||
}
|
||||
float severity = AIObjectiveExtinguishFires.GetFireSeverity(targetHull);
|
||||
if (severity > 0.75f && !isOrder &&
|
||||
targetHull.RoomName != null &&
|
||||
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Ignore severe fires to prevent casualities unless ordered to extinguish.
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
distanceFactor =
|
||||
GetDistanceFactor(
|
||||
new Vector2(character.WorldPosition.Y, characterY),
|
||||
targetHull.WorldPosition,
|
||||
verticalDistanceMultiplier: 3,
|
||||
maxDistance: 5000,
|
||||
factorAtMaxDistance: 0.1f);
|
||||
}
|
||||
|
||||
if (!inDamageRange && severity > 0.75f && distanceFactor < 0.75f && !isOrder && character.IsOnPlayerTeam &&
|
||||
targetHull.RoomName != null &&
|
||||
!targetHull.RoomName.Contains("reactor", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("engine", StringComparison.OrdinalIgnoreCase) &&
|
||||
!targetHull.RoomName.Contains("command", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Bots in the player crew ignore severe fires that are not close to the target to prevent casualties unless ordered to extinguish.
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
return Priority;
|
||||
}
|
||||
|
||||
@@ -81,16 +80,16 @@ namespace Barotrauma
|
||||
private float sinTime;
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher".ToIdentifier());
|
||||
var extinguisherItem = character.Inventory.FindItemByTag(Tags.FireExtinguisher);
|
||||
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
|
||||
{
|
||||
TryAddSubObjective(ref getExtinguisherObjective, () =>
|
||||
{
|
||||
if (character.IsOnPlayerTeam && !character.HasEquippedItem("fireextinguisher".ToIdentifier(), allowBroken: false))
|
||||
if (character.IsOnPlayerTeam && !character.HasEquippedItem(Tags.FireExtinguisher, allowBroken: false))
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, "findextinguisher".ToIdentifier(), 30.0f);
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher").Value, null, 2.0f, Tags.FireExtinguisher, 30.0f);
|
||||
}
|
||||
var getItemObjective = new AIObjectiveGetItem(character, "fireextinguisher".ToIdentifier(), objectiveManager, equip: true)
|
||||
var getItemObjective = new AIObjectiveGetItem(character, Tags.FireExtinguisher, objectiveManager, equip: true)
|
||||
{
|
||||
AllowStealing = true,
|
||||
// If the item is inside an unsafe hull, decrease the priority
|
||||
@@ -124,7 +123,9 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
float xDist = Math.Abs(character.WorldPosition.X - fs.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.CurrentHull.WorldPosition.Y - targetHull.WorldPosition.Y);
|
||||
// If fire source and the character are on the same level, it's better to ignore the y-axis (e.g. it doesn't matter if we stand or crouch), as the fire size is rectangular.
|
||||
// If we'd do this while climbing, the character would often get too close to the fire.
|
||||
float yDist = !character.IsClimbing && MathUtils.NearlyEqual(character.CurrentHull.WorldPosition.Y, targetHull.WorldPosition.Y) ? 0.0f : Math.Abs(character.CurrentHull.WorldPosition.Y - fs.WorldPosition.Y);
|
||||
float dist = xDist + yDist;
|
||||
bool inRange = dist < extinguisher.Range;
|
||||
bool isInDamageRange = fs.IsInDamageRange(character, fs.DamageRange) && character.CanSeeTarget(targetHull);
|
||||
@@ -153,8 +154,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(fs, character, objectiveManager, closeEnough: extinguisher.Range * 0.8f)
|
||||
{
|
||||
DialogueIdentifier = "dialogcannotreachfire".ToIdentifier(),
|
||||
TargetName = fs.Hull.DisplayName,
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachFire,
|
||||
TargetName = fs.Hull.DisplayName
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref gotoObjective)))
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
|
||||
if (target.IsHandcuffed && target.IsKnockedDown) { return false; }
|
||||
if (target.IsHandcuffed) { return false; }
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
+6
-6
@@ -21,7 +21,7 @@ namespace Barotrauma
|
||||
private Item targetItem;
|
||||
private int? oxygenSourceSlotIndex;
|
||||
|
||||
public const float MIN_OXYGEN = 10;
|
||||
private const float MinOxygen = 10;
|
||||
|
||||
protected override bool CheckObjectiveState() =>
|
||||
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
|
||||
@@ -154,9 +154,10 @@ namespace Barotrauma
|
||||
{
|
||||
AllowToFindDivingGear = false,
|
||||
AllowDangerousPressure = true,
|
||||
ConditionLevel = MIN_OXYGEN,
|
||||
ConditionLevel = MinOxygen,
|
||||
RemoveExistingWhenNecessary = true,
|
||||
TargetSlot = oxygenSourceSlotIndex
|
||||
TargetSlot = oxygenSourceSlotIndex,
|
||||
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _)
|
||||
};
|
||||
if (container.HasSubContainers)
|
||||
{
|
||||
@@ -199,7 +200,7 @@ namespace Barotrauma
|
||||
int ReportOxygenTankCount()
|
||||
{
|
||||
if (character.Submarine != Submarine.MainSub) { return 1; }
|
||||
int remainingOxygenTanks = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1);
|
||||
int remainingOxygenTanks = Submarine.MainSub?.GetItems(false).Count(i => i.HasTag(Tags.OxygenSource) && i.Condition > 1) ?? 0;
|
||||
if (remainingOxygenTanks == 0)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogOutOfOxygenTanks").Value, null, 0.0f, "outofoxygentanks".ToIdentifier(), 30.0f);
|
||||
@@ -227,7 +228,6 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private bool IsSuitableContainedOxygenSource(Item item)
|
||||
{
|
||||
return
|
||||
@@ -259,7 +259,7 @@ namespace Barotrauma
|
||||
// The margin helps us to survive, because we might need some oxygen before we can find more oxygen.
|
||||
// When we are venturing outside of our sub, let's just suppose that we have enough oxygen with us and optimize it so that we don't keep switching off half used tanks.
|
||||
float min = 0.01f;
|
||||
float minOxygen = character.IsInFriendlySub ? MIN_OXYGEN : min;
|
||||
float minOxygen = character.IsInFriendlySub ? MinOxygen : min;
|
||||
if (minOxygen > min && character.Inventory.AllItems.Any(i => i.HasTag(Tags.OxygenSource) && i.ConditionPercentage >= minOxygen))
|
||||
{
|
||||
// There's a valid oxygen tank in the inventory -> no need to swap the tank too early.
|
||||
|
||||
+59
-13
@@ -42,9 +42,9 @@ namespace Barotrauma
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
Priority = (
|
||||
objectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Priority > 0) ||
|
||||
objectiveManager.CurrentOrder is AIObjectiveGoTo ||
|
||||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
objectiveManager.Objectives.Any(o => (o is AIObjectiveCombat || o is AIObjectiveReturn) && o.Priority > 0))
|
||||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat or AIObjectiveReturn && o.Priority > 0))
|
||||
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
|
||||
}
|
||||
else
|
||||
@@ -71,6 +71,11 @@ namespace Barotrauma
|
||||
Priority = AIObjectiveManager.MaxObjectivePriority;
|
||||
}
|
||||
}
|
||||
else if (objectiveManager.CurrentOrder is AIObjectiveGoTo { IsFollowOrder: true })
|
||||
{
|
||||
// Ordered to follow -> Don't flee from the enemies/fires (doesn't get here if we need more oxygen).
|
||||
Priority = 0;
|
||||
}
|
||||
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
|
||||
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
|
||||
{
|
||||
@@ -83,7 +88,7 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
}
|
||||
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
||||
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
|
||||
if (divingGearObjective is { IsCompleted: false, CanBeCompleted: true, Priority: > 0f })
|
||||
{
|
||||
// Boost the priority while seeking the diving gear
|
||||
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
|
||||
@@ -149,7 +154,13 @@ namespace Barotrauma
|
||||
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
|
||||
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
|
||||
{
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
|
||||
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit, objectiveManager);
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine?.Info is { IsOutpost: true })
|
||||
{
|
||||
// In outposts, the NPCs don't try to use diving suits, because otherwise there's probably not enough for those trying to fix the leaks.
|
||||
// This is not a hard rule: the bots may still grab a suit, unless they find a diving mask.
|
||||
needsDivingSuit = false;
|
||||
}
|
||||
bool needsEquipment = shouldActOnSuffocation;
|
||||
if (needsDivingSuit)
|
||||
{
|
||||
@@ -307,10 +318,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
if (escapeVel != Vector2.Zero)
|
||||
if (escapeVel != Vector2.Zero && character.CurrentHull is Hull currentHull)
|
||||
{
|
||||
float left = character.CurrentHull.Rect.X + 50;
|
||||
float right = character.CurrentHull.Rect.Right - 50;
|
||||
float left = currentHull.Rect.X + 50;
|
||||
float right = currentHull.Rect.Right - 50;
|
||||
//only move if we haven't reached the edge of the room
|
||||
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
|
||||
{
|
||||
@@ -441,7 +452,7 @@ namespace Barotrauma
|
||||
hullSearchIndex = 0;
|
||||
#if DEBUG
|
||||
stopWatch.Stop();
|
||||
DebugConsole.NewMessage($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms", debugOnly: true);
|
||||
DebugConsole.Log($"({character.DisplayName}) Sorted hulls by suitability in {stopWatch.ElapsedMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -471,12 +482,47 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
// Each unsafe node reduces the hull safety value.
|
||||
// Ignore the current hull, because otherwise we couldn't find a path out.
|
||||
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
|
||||
hullSafety /= 1 + unsafeNodes;
|
||||
// Check the path safety. Each unsafe node reduces the hull safety value.
|
||||
Hull previousHull = null;
|
||||
foreach (WayPoint node in path.Nodes)
|
||||
{
|
||||
Hull hull = node.CurrentHull;
|
||||
if (hull == previousHull)
|
||||
{
|
||||
// Let's evaluate each hull only once. If we'd want to make this foolproof, we'd have to add the checked hulls to a list,
|
||||
// yet in practice there shouldn't be a case where the path would get back to a hull once it has exited it.
|
||||
continue;
|
||||
}
|
||||
previousHull = hull;
|
||||
if (hull == character.CurrentHull)
|
||||
{
|
||||
// Ignore the current hull, because otherwise we couldn't find a path out.
|
||||
continue;
|
||||
}
|
||||
if (HumanAIController.UnsafeHulls.Contains(hull))
|
||||
{
|
||||
// Compare safety of the node hull to the current hull safety.
|
||||
float nodeHullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
|
||||
if (nodeHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD && nodeHullSafety < HumanAIController.CurrentHullSafety)
|
||||
{
|
||||
// If the node hull is considered unsafe and less safe than the current hull, let's ignore the target.
|
||||
hullSafety = 0;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, each unsafe hull on the path reduces the safety of the target hull by 50% of their threat value.
|
||||
float hullThreat = 100 - nodeHullSafety;
|
||||
hullSafety -= hullThreat / 2;
|
||||
if (hullSafety <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, includingConnectedSubs: true))
|
||||
{
|
||||
hullSafety /= 10;
|
||||
}
|
||||
|
||||
+27
-12
@@ -83,7 +83,7 @@ namespace Barotrauma
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsValidTarget(target, character)) { return false; }
|
||||
if (!CheckTarget(target)) { return false; }
|
||||
float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance;
|
||||
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; }
|
||||
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
|
||||
@@ -145,26 +145,31 @@ namespace Barotrauma
|
||||
// Might be e.g. sitting on a chair.
|
||||
character.SelectedSecondaryItem = null;
|
||||
}
|
||||
foreach (var target in Character.CharacterList)
|
||||
if (HumanAIController.CurrentHullSafety >= HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
if (!IsValidTarget(target, character)) { continue; }
|
||||
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
|
||||
if (target.Inventory.AllItems.Any(it => it.Illegitimate && target.HasEquippedItem(it)) &&
|
||||
character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
foreach (var target in Character.CharacterList)
|
||||
{
|
||||
AIObjectiveCheckStolenItems? existingObjective =
|
||||
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
|
||||
if (existingObjective == null)
|
||||
if (!CheckTarget(target)) { continue; }
|
||||
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
|
||||
if (target.Inventory.AllItems.Any(it => target.HasEquippedItem(it) && AIObjectiveCheckStolenItems.IsItemIllegitimate(target, it)) && character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
{
|
||||
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
if (HumanAIController.CalculateObjectiveHullSafety(target) >= HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
// Don't do inspections in unsafe hulls, because under a threat, bots are allowed to wear diving gear or hold fire extinguishers etc. Even if they are "stolen".
|
||||
AIObjectiveCheckStolenItems? existingObjective = objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
|
||||
if (existingObjective == null)
|
||||
{
|
||||
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
|
||||
}
|
||||
|
||||
private bool IsValidTarget(Character target, Character character)
|
||||
private bool CheckTarget(Character target)
|
||||
{
|
||||
if (target == null || target.Removed) { return false; }
|
||||
if (target.IsIncapacitated) { return false; }
|
||||
@@ -192,6 +197,16 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
|
||||
{
|
||||
MarkTargetAsInspected(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the targets as being inspected for stolen items (e.g. while arresting the character),
|
||||
/// meaning characters with this objective won't attempt to trigger an inspection in a while.
|
||||
/// </summary>
|
||||
/// <param name="target"></param>
|
||||
public static void MarkTargetAsInspected(Character target)
|
||||
{
|
||||
lastInspectionTimes[target] = Timing.TotalTime;
|
||||
}
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ namespace Barotrauma
|
||||
{
|
||||
UseDistanceRelativeToAimSourcePos = true,
|
||||
CloseEnough = reach,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak".ToIdentifier() : Identifier.Empty,
|
||||
DialogueIdentifier = Leak.FlowTargetHull != null ? AIObjectiveGoTo.DialogCannotReachLeak : Identifier.Empty,
|
||||
TargetName = Leak.FlowTargetHull?.DisplayName,
|
||||
requiredCondition = () =>
|
||||
Leak.Submarine == character.Submarine &&
|
||||
|
||||
+26
-1
@@ -31,6 +31,10 @@ namespace Barotrauma
|
||||
|
||||
private Item targetItem;
|
||||
private readonly Item originalTarget;
|
||||
/// <summary>
|
||||
/// ItemContainer the bot is trying to put the <see cref="TargetItem"/> into. Only set when the objective is a subobjective of a <see cref="AIObjectiveContainItem"/>.
|
||||
/// </summary>
|
||||
public ItemContainer ContainTarget;
|
||||
private ISpatialEntity moveToTarget;
|
||||
private bool isDoneSeeking;
|
||||
public Item TargetItem => targetItem;
|
||||
@@ -76,6 +80,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public InvSlotType? EquipSlotType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tags of items that bots are allowed to take from outposts, when needed. For example when there's not enough oxygen in the room, or if they need to extinguish a fire.
|
||||
/// The guards won't react if these items are taken by the bots.
|
||||
/// </summary>
|
||||
public static readonly Identifier[] AllowedItemsToTake = { Tags.OxygenSource, Tags.FireExtinguisher, Tags.LightDivingGear, Tags.HeavyDivingGear };
|
||||
|
||||
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
@@ -477,7 +487,17 @@ namespace Barotrauma
|
||||
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
|
||||
if (ownerItem != item.Container)
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
if (ContainTarget != null && ContainTarget.Item.Prefab.Identifier == item.Container.Prefab.Identifier)
|
||||
{
|
||||
// The item is identical to the item we are trying to contain the item to (e.g. trying to find an oxygen source to a mask -> allow to take oxygen sources from other masks)
|
||||
// Reduce the priority just a tiny bit, so that we choose items that are not inside the items first.
|
||||
// TODO: Doesn't solve the issue for items that are not the same type but that should be treated the same. E.g. diving mask and clown diving mask.
|
||||
itemPriority = 0.95f;
|
||||
}
|
||||
else
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,6 +665,11 @@ namespace Barotrauma
|
||||
if (prefab is not ItemPrefab itemPrefab) { continue; }
|
||||
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
|
||||
{
|
||||
if (character.AIController.HasInfiniteItemSpawns(prefab.Identifier))
|
||||
{
|
||||
// If an item with infinite spawns is defined, let's use it.
|
||||
return itemPrefab;
|
||||
}
|
||||
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
|
||||
itemPrefab.DefaultPrice.Price :
|
||||
float.MaxValue;
|
||||
|
||||
+104
-24
@@ -95,7 +95,32 @@ namespace Barotrauma
|
||||
protected override bool AllowOutsideSubmarine => AllowGoingOutside;
|
||||
protected override bool AllowInAnySub => true;
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a target.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachTarget = "dialogcannotreachtarget".ToIdentifier();
|
||||
/// <summary>
|
||||
/// Generic NPC line for when the NPC fails to find a path to some place/target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachPlace = "dialogcannotreachplace".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a patient they're trying to treat.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the target.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachPatient = "dialogcannotreachpatient".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a fire they're trying to extinguish.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachFire = "dialogcannotreachfire".ToIdentifier();
|
||||
/// <summary>
|
||||
/// NPC line for when the NPC fails to find a path to a leak they're trying to fix.
|
||||
/// Note that this line includes the tag [name], which needs to be replaced with the name of the room the NPC is trying to get to.
|
||||
/// </summary>
|
||||
public static readonly Identifier DialogCannotReachLeak = "dialogcannotreachleak".ToIdentifier();
|
||||
|
||||
public Identifier DialogueIdentifier { get; set; } = DialogCannotReachPlace;
|
||||
private readonly Identifier ExoSuitRefuel = "dialog.exosuit.refuel".ToIdentifier();
|
||||
private readonly Identifier ExoSuitOutOfFuel = "dialog.exosuit.outoffuel".ToIdentifier();
|
||||
|
||||
@@ -116,12 +141,12 @@ namespace Barotrauma
|
||||
Abandon = !isOrder;
|
||||
return Priority;
|
||||
}
|
||||
if (Target == null || Target is Entity e && e.Removed)
|
||||
if (Target is null or Entity { Removed: true })
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !isOrder;
|
||||
}
|
||||
if (IgnoreIfTargetDead && Target is Character character && character.IsDead)
|
||||
if (IgnoreIfTargetDead && Target is Character { IsDead: true })
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = !isOrder;
|
||||
@@ -182,6 +207,17 @@ namespace Barotrauma
|
||||
if (DialogueIdentifier == null) { return; }
|
||||
if (!SpeakIfFails) { return; }
|
||||
if (SpeakCannotReachCondition != null && !SpeakCannotReachCondition()) { return; }
|
||||
|
||||
if (TargetName == null && DialogueIdentifier == DialogCannotReachTarget)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(
|
||||
$"Error in {nameof(SpeakCannotReach)}: "+
|
||||
$"attempted to use a dialog line that mentions the target (dialogue identifier: {DialogueIdentifier}), but the name of the target ({(Target?.ToString() ?? "null")}) isn't set.");
|
||||
#endif
|
||||
DialogueIdentifier = DialogCannotReachPlace;
|
||||
}
|
||||
|
||||
LocalizedString msg = TargetName == null ?
|
||||
TextManager.Get(DialogueIdentifier) :
|
||||
TextManager.GetWithVariable(DialogueIdentifier, "[name]".ToIdentifier(), TargetName, formatCapitals: Target is Character ? FormatCapitals.No : FormatCapitals.Yes);
|
||||
@@ -342,34 +378,43 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
if (Abandon) { return; }
|
||||
if (getDivingGearIfNeeded)
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
Character followTarget = Target as Character;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
{
|
||||
Character followTarget = Target as Character;
|
||||
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
|
||||
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
|
||||
bool tryToGetDivingSuit = needsDivingSuit;
|
||||
if (Mimic && !character.IsImmuneToPressure)
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
{
|
||||
if (HumanAIController.HasDivingSuit(followTarget))
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
tryToGetDivingGear = true;
|
||||
tryToGetDivingSuit = true;
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
|
||||
tryToGetDivingGear = true;
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
}
|
||||
bool needsEquipment = false;
|
||||
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
|
||||
if (tryToGetDivingSuit)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
|
||||
}
|
||||
else if (tryToGetDivingGear)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
}
|
||||
if (!getDivingGearIfNeeded)
|
||||
{
|
||||
if (needsEquipment)
|
||||
{
|
||||
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
// Don't try to reach the target without proper equipment.
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.LockHands)
|
||||
{
|
||||
cantFindDivingGear = true;
|
||||
@@ -794,6 +839,11 @@ namespace Barotrauma
|
||||
// Going through a hatch
|
||||
return false;
|
||||
}
|
||||
if (Target is Item targetItem && targetItem.GetComponent<Pickable>() == null)
|
||||
{
|
||||
// Targeting a static item, such as a reactor or a controller -> Don't complete, until we are no longer climbing.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!AlwaysUseEuclideanDistance && !character.AnimController.InWater)
|
||||
@@ -893,5 +943,35 @@ namespace Barotrauma
|
||||
pathSteering.ResetPath();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldRun(bool run)
|
||||
{
|
||||
if (run && objectiveManager.ForcedOrder == this && IsWaitOrder && !character.IsOnPlayerTeam)
|
||||
{
|
||||
// NPCs with a wait order don't run.
|
||||
run = false;
|
||||
}
|
||||
else if (Target != null)
|
||||
{
|
||||
if (character.CurrentHull == null)
|
||||
{
|
||||
run = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > 300 * 300;
|
||||
}
|
||||
else
|
||||
{
|
||||
float yDiff = Target.WorldPosition.Y - character.WorldPosition.Y;
|
||||
if (Math.Abs(yDiff) > 100)
|
||||
{
|
||||
run = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDiff = Target.WorldPosition.X - character.WorldPosition.X;
|
||||
run = Math.Abs(xDiff) > 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
return run;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+132
-105
@@ -185,135 +185,162 @@ namespace Barotrauma
|
||||
IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !currentTargetIsInvalid && !IsForbidden(TargetHull))
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
|
||||
if (stayInHull)
|
||||
if (HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else if (currentTarget != null)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
// Ask to refresh, because otherwise we can't get back to the hull.
|
||||
HumanAIController.AskToRecalculateHullSafety(TargetHull);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
PathSteering.Reset();
|
||||
currentTarget = TargetHull;
|
||||
NavigateTo(currentTarget);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget.Submarine != character.Submarine)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
|
||||
{
|
||||
if (newTargetTimer > timerMargin)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
else if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
|
||||
{
|
||||
// Don't allow new targets when climbing straight up or down
|
||||
SetTargetTimerHigh();
|
||||
}
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
if (newTargetTimer <= 0.0f)
|
||||
{
|
||||
if (!searchingNewHull)
|
||||
{
|
||||
//find all available hulls first
|
||||
searchingNewHull = true;
|
||||
FindTargetHulls();
|
||||
}
|
||||
else if (targetHulls.Any())
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe hulls on the way to the target
|
||||
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
|
||||
return true;
|
||||
//don't stop at ladders when idling
|
||||
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
if (path.Unreachable)
|
||||
{
|
||||
//can't go to this room, remove it from the list and try another room
|
||||
int index = targetHulls.IndexOf(currentTarget);
|
||||
targetHulls.RemoveAt(index);
|
||||
hullWeights.RemoveAt(index);
|
||||
PathSteering.Reset();
|
||||
currentTarget = null;
|
||||
SetTargetTimerLow();
|
||||
return;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a valid hull
|
||||
SetTargetTimerHigh();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
}
|
||||
newTargetTimer -= deltaTime;
|
||||
if (currentTarget == null || PathSteering.CurrentPath == null)
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget != null && !currentTargetIsInvalid)
|
||||
NavigateTo(currentTarget);
|
||||
}
|
||||
|
||||
void NavigateTo(Hull target)
|
||||
{
|
||||
bool isAtTarget = character.CurrentHull == target && IsSteeringFinished();
|
||||
if (isAtTarget)
|
||||
{
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted)
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget.Submarine.TeamID != character.TeamID)
|
||||
StopMoving();
|
||||
if (character.AnimController.GetHeightFromFloor() < character.AnimController.ImpactTolerance / 2)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
character.StopClimbing();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentTarget.Submarine != character.Submarine)
|
||||
{
|
||||
currentTargetIsInvalid = true;
|
||||
}
|
||||
Wander(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
|
||||
else if (target != null)
|
||||
{
|
||||
if (newTargetTimer > timerMargin)
|
||||
{
|
||||
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
|
||||
//almost constantly when there's a small number of potential hulls to move to
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
else if (character.IsClimbing)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
|
||||
{
|
||||
// Don't allow new targets when climbing straight up or down
|
||||
SetTargetTimerHigh();
|
||||
}
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (currentTarget == null)
|
||||
{
|
||||
SetTargetTimerLow();
|
||||
}
|
||||
}
|
||||
if (newTargetTimer <= 0.0f)
|
||||
{
|
||||
if (!searchingNewHull)
|
||||
{
|
||||
//find all available hulls first
|
||||
searchingNewHull = true;
|
||||
FindTargetHulls();
|
||||
}
|
||||
else if (targetHulls.Any())
|
||||
{
|
||||
//choose a random available hull
|
||||
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
|
||||
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
|
||||
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
|
||||
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
|
||||
{
|
||||
if (node.Waypoint.CurrentHull == null) { return false; }
|
||||
// Check that there is no unsafe hulls on the way to the target
|
||||
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
|
||||
return true;
|
||||
//don't stop at ladders when idling
|
||||
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
|
||||
if (path.Unreachable)
|
||||
{
|
||||
//can't go to this room, remove it from the list and try another room
|
||||
int index = targetHulls.IndexOf(currentTarget);
|
||||
targetHulls.RemoveAt(index);
|
||||
hullWeights.RemoveAt(index);
|
||||
PathSteering.Reset();
|
||||
currentTarget = null;
|
||||
SetTargetTimerLow();
|
||||
return;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Couldn't find a valid hull
|
||||
SetTargetTimerHigh();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
}
|
||||
newTargetTimer -= deltaTime;
|
||||
if (!character.IsClimbing && (PathSteering == null || PathSteering.CurrentPath == null || IsSteeringFinished()))
|
||||
{
|
||||
Wander(deltaTime);
|
||||
}
|
||||
else if (currentTarget != null)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
|
||||
nodeFilter: node => node.Waypoint.CurrentHull != null,
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
|
||||
PathTo(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
PathSteering.Reset();
|
||||
StopMoving();
|
||||
}
|
||||
}
|
||||
|
||||
void StopMoving()
|
||||
{
|
||||
SteeringManager.Reset();
|
||||
PathSteering.ResetPath();
|
||||
}
|
||||
|
||||
void PathTo(ISpatialEntity target)
|
||||
{
|
||||
PathSteering.SteeringSeek(character.GetRelativeSimPosition(target), weight: 1,
|
||||
nodeFilter: node => node.Waypoint.CurrentHull != null,
|
||||
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Wander(float deltaTime)
|
||||
|
||||
+11
-10
@@ -22,7 +22,7 @@ namespace Barotrauma
|
||||
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
|
||||
|
||||
private int itemIndex;
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private AIObjectiveMoveItem moveItemObjective;
|
||||
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
private Item targetItem;
|
||||
private readonly string abandonGetItemDialogueIdentifier = "dialogcannotfindloadable";
|
||||
@@ -196,17 +196,17 @@ namespace Barotrauma
|
||||
float devotion = (CumulatedDevotion + (hasContainable ? 100 - MaxDevotion : 0)) / 100;
|
||||
float max = AIObjectiveManager.LowestOrderPriority - (hasContainable ? 1 : 2);
|
||||
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (distanceFactor * PriorityModifier), 0, 1));
|
||||
if (decontainObjective != null && targetItem.Container != Container)
|
||||
if (moveItemObjective != null && targetItem.Container != Container)
|
||||
{
|
||||
if (!IsValidContainable(targetItem))
|
||||
{
|
||||
// Target is not valid anymore, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
moveItemObjective.Abandon = true;
|
||||
}
|
||||
else if (!ItemContainer.Inventory.CanBePut(targetItem) && ItemContainer.Inventory.AllItems.None(i => AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition)))
|
||||
{
|
||||
// The container is full and there's no item that should be removed, abandon the objective
|
||||
decontainObjective.Abandon = true;
|
||||
moveItemObjective.Abandon = true;
|
||||
}
|
||||
}
|
||||
if (ItemContainer.Inventory.IsFull())
|
||||
@@ -257,26 +257,27 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if(decontainObjective == null && !IsValidContainable(targetItem))
|
||||
if(moveItemObjective == null && !IsValidContainable(targetItem))
|
||||
{
|
||||
IgnoreTargetItem();
|
||||
Reset();
|
||||
return;
|
||||
}
|
||||
TryAddSubObjective(ref decontainObjective,
|
||||
constructor: () => new AIObjectiveDecontainItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
|
||||
TryAddSubObjective(ref moveItemObjective,
|
||||
constructor: () => new AIObjectiveMoveItem(character, targetItem, objectiveManager, targetContainer: ItemContainer, priorityModifier: PriorityModifier)
|
||||
{
|
||||
AbandonGetItemDialogueCondition = () => IsValidContainable(targetItem),
|
||||
AbandonGetItemDialogueIdentifier = abandonGetItemDialogueIdentifier,
|
||||
Equip = true,
|
||||
RemoveExistingWhenNecessary = true,
|
||||
RemoveExistingPredicate = (i) => !ValidContainableItemIdentifiers.Contains(i.Prefab.Identifier) || AIObjectiveLoadItems.ItemMatchesTargetCondition(i, TargetItemCondition),
|
||||
RemoveExistingMax = 1
|
||||
RemoveExistingMax = 1,
|
||||
AllowToFindDivingGear = objectiveManager.HasOrder<AIObjectiveLoadItems>()
|
||||
},
|
||||
onCompleted: () =>
|
||||
{
|
||||
IsCompleted = true;
|
||||
RemoveSubObjective(ref decontainObjective);
|
||||
RemoveSubObjective(ref moveItemObjective);
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -324,7 +325,7 @@ namespace Barotrauma
|
||||
{
|
||||
base.Reset();
|
||||
// Don't reset the target item when resetting the objective because it affects priority calculations
|
||||
decontainObjective = null;
|
||||
moveItemObjective = null;
|
||||
itemIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
+15
-4
@@ -35,7 +35,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public const float HighestOrderPriority = 70;
|
||||
/// <summary>
|
||||
/// Maximum priority of an order given to the character (rightmost order in the crew list)
|
||||
/// Minimum priority of an order given to the character (rightmost order in the crew list)
|
||||
/// </summary>
|
||||
public const float LowestOrderPriority = 60;
|
||||
/// <summary>
|
||||
@@ -485,7 +485,7 @@ namespace Barotrauma
|
||||
IgnoreIfTargetDead = true,
|
||||
IsFollowOrder = true,
|
||||
Mimic = character.IsOnPlayerTeam,
|
||||
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
|
||||
};
|
||||
break;
|
||||
case "wait":
|
||||
@@ -724,6 +724,11 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current objective or its currently active subobjective (first in chain), regadless of the type.
|
||||
/// Note: Not recursive, and thus doesn't work for deeper hierarchy!
|
||||
/// For seeking objectives of specific type and in a deep hierarchy, use <see cref="GetLastActiveObjective{T}"/> or with looping objectives <see cref="GetFirstActiveObjective{T}"/>
|
||||
/// </summary>
|
||||
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
|
||||
|
||||
/// <summary>
|
||||
@@ -740,7 +745,8 @@ namespace Barotrauma
|
||||
/// Returns the last active objective of the specified objective type.
|
||||
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
|
||||
/// </summary>
|
||||
/// <returns>The last active objective of the specified type if found.
|
||||
/// <returns>
|
||||
/// The last active objective of the specified type if found.
|
||||
/// </returns>
|
||||
public T GetLastActiveObjective<T>() where T : AIObjective
|
||||
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
|
||||
@@ -763,7 +769,12 @@ namespace Barotrauma
|
||||
if (CurrentObjective == null) { return Enumerable.Empty<T>(); }
|
||||
return CurrentObjective.GetSubObjectivesRecursive(includingSelf: true).OfType<T>();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is the current objective or any of its subobjectives of the given type?
|
||||
/// Useful for checking whether the bot has a certain type of objective active in the hierarchy.
|
||||
/// </summary>
|
||||
/// <returns>False for objectives and orders that are not currently active.</returns>
|
||||
public bool HasActiveObjective<T>() where T : AIObjective => CurrentObjective is T || CurrentObjective != null && CurrentObjective.GetSubObjectivesRecursive().Any(so => so is T);
|
||||
|
||||
public bool IsOrder(AIObjective objective)
|
||||
|
||||
+25
-16
@@ -6,9 +6,9 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveDecontainItem : AIObjective
|
||||
class AIObjectiveMoveItem : AIObjective
|
||||
{
|
||||
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
|
||||
public override Identifier Identifier { get; set; } = "move item".ToIdentifier();
|
||||
protected override bool AllowWhileHandcuffed => false;
|
||||
|
||||
public Func<Item, float> GetItemPriority;
|
||||
@@ -47,8 +47,15 @@ namespace Barotrauma
|
||||
public int? RemoveExistingMax { get; set; }
|
||||
public string AbandonGetItemDialogueIdentifier { get; set; }
|
||||
public Func<bool> AbandonGetItemDialogueCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// By default, finding diving gear is not allowed here, because it can cause unexpected behavior in most use cases.
|
||||
/// E.g. bots equipping diving suits to clean up some items in flooded rooms.
|
||||
/// Sometimes, at least when used in orders, we might want to allow this. See <see cref="AIObjectiveLoadItem"/>.
|
||||
/// </summary>
|
||||
public bool AllowToFindDivingGear { get; set; }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, ItemContainer sourceContainer = null, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.targetItem = targetItem;
|
||||
@@ -56,10 +63,10 @@ namespace Barotrauma
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Identifier itemIdentifier, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: this(character, new Identifier[] { itemIdentifier }, objectiveManager, sourceContainer, targetContainer, priorityModifier) { }
|
||||
|
||||
public AIObjectiveDecontainItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
public AIObjectiveMoveItem(Character character, Identifier[] itemIdentifiers, AIObjectiveManager objectiveManager, ItemContainer sourceContainer, ItemContainer targetContainer = null, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
@@ -75,16 +82,16 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
Item itemToDecontain =
|
||||
Item itemToMove =
|
||||
targetItem ??
|
||||
sourceContainer.Inventory.FindItem(i => itemIdentifiers.Any(id => i.Prefab.Identifier == id || i.HasTag(id) && !i.IgnoreByAI(character)), recursive: false);
|
||||
|
||||
if (itemToDecontain == null)
|
||||
if (itemToMove == null)
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.IgnoreByAI(character))
|
||||
if (itemToMove.IgnoreByAI(character))
|
||||
{
|
||||
Abandon = true;
|
||||
return;
|
||||
@@ -96,19 +103,19 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
return;
|
||||
}
|
||||
if (itemToDecontain.Container != sourceContainer.Item)
|
||||
if (itemToMove.Container != sourceContainer.Item)
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
itemToMove.Drop(character);
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (targetContainer.Inventory.Contains(itemToDecontain))
|
||||
else if (targetContainer.Inventory.Contains(itemToMove))
|
||||
{
|
||||
IsCompleted = true;
|
||||
return;
|
||||
}
|
||||
if (getItemObjective == null && !itemToDecontain.IsOwnedBy(character))
|
||||
if (getItemObjective == null && !itemToMove.IsOwnedBy(character))
|
||||
{
|
||||
TryAddSubObjective(ref getItemObjective,
|
||||
constructor: () => new AIObjectiveGetItem(character, targetItem, objectiveManager, Equip)
|
||||
@@ -116,7 +123,8 @@ namespace Barotrauma
|
||||
CannotFindDialogueCondition = AbandonGetItemDialogueCondition,
|
||||
CannotFindDialogueIdentifierOverride = AbandonGetItemDialogueIdentifier,
|
||||
SpeakIfFails = AbandonGetItemDialogueIdentifier != null,
|
||||
TakeWholeStack = this.TakeWholeStack
|
||||
TakeWholeStack = TakeWholeStack,
|
||||
AllowToFindDivingGear = AllowToFindDivingGear
|
||||
},
|
||||
onAbandon: () => Abandon = true);
|
||||
return;
|
||||
@@ -124,7 +132,7 @@ namespace Barotrauma
|
||||
if (targetContainer != null)
|
||||
{
|
||||
TryAddSubObjective(ref containObjective,
|
||||
constructor: () => new AIObjectiveContainItem(character, itemToDecontain, targetContainer, objectiveManager)
|
||||
constructor: () => new AIObjectiveContainItem(character, itemToMove, targetContainer, objectiveManager)
|
||||
{
|
||||
MoveWholeStack = TakeWholeStack,
|
||||
Equip = Equip,
|
||||
@@ -133,14 +141,15 @@ namespace Barotrauma
|
||||
RemoveExistingPredicate = RemoveExistingPredicate,
|
||||
RemoveMax = RemoveExistingMax,
|
||||
GetItemPriority = GetItemPriority,
|
||||
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet()
|
||||
ignoredContainerIdentifiers = sourceContainer?.Item.Prefab.Identifier.ToEnumerable().ToImmutableHashSet(),
|
||||
AllowToFindDivingGear = AllowToFindDivingGear
|
||||
},
|
||||
onCompleted: () => IsCompleted = true,
|
||||
onAbandon: () => Abandon = true);
|
||||
}
|
||||
else
|
||||
{
|
||||
itemToDecontain.Drop(character);
|
||||
itemToMove.Drop(character);
|
||||
IsCompleted = true;
|
||||
}
|
||||
}
|
||||
+15
-5
@@ -1,5 +1,5 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -89,6 +89,17 @@ namespace Barotrauma
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
Hull targetHull = targetItem.CurrentHull;
|
||||
if (HumanAIController.UnsafeHulls.Contains(targetHull))
|
||||
{
|
||||
// Ignore the objective, if the target hull is dangerous.
|
||||
Priority = 0;
|
||||
if (isOrder && this == objectiveManager.CurrentObjective && character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("dialogoperatetargetroomisunsafe", "[item]", targetItem.Name).Value, delay: 1.0f, identifier: "dialogoperatetargetroomisunsafe".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
var reactor = component.Item.GetComponent<Reactor>();
|
||||
if (reactor != null)
|
||||
{
|
||||
@@ -137,10 +148,8 @@ namespace Barotrauma
|
||||
}
|
||||
if (targetItem.CurrentHull == null ||
|
||||
targetItem.Submarine != character.Submarine && !isOrder ||
|
||||
targetItem.CurrentHull.FireSources.Any() ||
|
||||
IsItemOperatedByAnother(target) ||
|
||||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))
|
||||
|| component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
|
||||
component.Item.IgnoreByAI(character) || useController && controller.Item.IgnoreByAI(character))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
@@ -246,6 +255,7 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(target.Item, character, objectiveManager, closeEnough: 50)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = target.Item.Name,
|
||||
endNodeFilter = EndNodeFilter ?? AIObjectiveGetItem.CreateEndNodeFilter(target.Item)
|
||||
},
|
||||
|
||||
+1
@@ -234,6 +234,7 @@ namespace Barotrauma
|
||||
{
|
||||
var objective = new AIObjectiveGoTo(Item, character, objectiveManager)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachTarget,
|
||||
TargetName = Item.Name,
|
||||
SpeakCannotReachCondition = () => isPriority
|
||||
};
|
||||
|
||||
+12
-9
@@ -161,7 +161,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
|
||||
TargetName = Target.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
@@ -197,13 +197,16 @@ namespace Barotrauma
|
||||
{
|
||||
RemoveSubObjective(ref replaceOxygenObjective);
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager)
|
||||
{
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPlace
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
onAbandon: () =>
|
||||
{
|
||||
RemoveSubObjective(ref goToObjective);
|
||||
safeHull = character.CurrentHull;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,7 +224,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(Target, character, objectiveManager)
|
||||
{
|
||||
CloseEnough = CloseEnoughToTreat,
|
||||
DialogueIdentifier = "dialogcannotreachpatient".ToIdentifier(),
|
||||
DialogueIdentifier = AIObjectiveGoTo.DialogCannotReachPatient,
|
||||
TargetName = Target.DisplayName
|
||||
},
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective),
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Barotrauma
|
||||
Target = GetReturnTarget(Submarine.MainSubs) ?? GetReturnTarget(Submarine.Loaded);
|
||||
if (Target == null)
|
||||
{
|
||||
if (GameMain.GameSession.GameMode is not TestGameMode)
|
||||
if (GameMain.GameSession?.GameMode is not TestGameMode)
|
||||
{
|
||||
DebugConsole.AddWarning($"({character.DisplayName}) No suitable return target found. Cannot return back to the main sub.");
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<PathNode> sortedNodes;
|
||||
|
||||
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
|
||||
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true, float outsideNodePenalty = 0)
|
||||
{
|
||||
foreach (PathNode node in nodes)
|
||||
{
|
||||
@@ -325,7 +325,12 @@ namespace Barotrauma
|
||||
#endif
|
||||
return new SteeringPath(true);
|
||||
}
|
||||
return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize);
|
||||
float outsideNodeCostPenalty = outsideNodePenalty;
|
||||
if (ApplyPenaltyToOutsideNodes)
|
||||
{
|
||||
outsideNodeCostPenalty += 100;
|
||||
}
|
||||
return FindPath(startNode, endNode, nodeFilter, errorMsgStr, minGapSize, outsideNodeCostPenalty);
|
||||
|
||||
bool IsValidStartNode(PathNode node) => IsValidNode(node, (isCharacter, start), startNodeFilter);
|
||||
|
||||
@@ -356,7 +361,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0)
|
||||
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "", float minGapSize = 0f, float outsideNodePenalty = 0f)
|
||||
{
|
||||
if (start == end)
|
||||
{
|
||||
@@ -398,50 +403,65 @@ namespace Barotrauma
|
||||
for (int i = 0; i < currNode.connections.Count; i++)
|
||||
{
|
||||
PathNode nextNode = currNode.connections[i];
|
||||
|
||||
//a node that hasn't been searched yet
|
||||
if (nextNode.state == 0)
|
||||
{
|
||||
nextNode.H = Vector2.Distance(nextNode.Position, end.Position);
|
||||
|
||||
float penalty = 0.0f;
|
||||
if (GetNodePenalty != null)
|
||||
switch (nextNode.state)
|
||||
{
|
||||
//a node that hasn't been searched yet
|
||||
case 0:
|
||||
{
|
||||
float? nodePenalty = GetNodePenalty(currNode, nextNode);
|
||||
if (nodePenalty == null)
|
||||
nextNode.H = Vector2.Distance(nextNode.Position, end.Position);
|
||||
float cost = CalculateNodeCost();
|
||||
if (cost < float.PositiveInfinity)
|
||||
{
|
||||
nextNode.state = -1;
|
||||
continue;
|
||||
nextNode.G = cost;
|
||||
nextNode.F = nextNode.G + nextNode.H;
|
||||
nextNode.Parent = currNode;
|
||||
nextNode.state = 1;
|
||||
}
|
||||
penalty = nodePenalty.Value;
|
||||
else
|
||||
{
|
||||
// Set searched and invalid.
|
||||
nextNode.state = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
//node that has been searched
|
||||
case 1 or -1:
|
||||
{
|
||||
float tempG = CalculateNodeCost();
|
||||
//only use if this new route is better than the
|
||||
//route the node was a part of
|
||||
if (tempG < nextNode.G)
|
||||
{
|
||||
nextNode.G = tempG;
|
||||
nextNode.F = nextNode.G + nextNode.H;
|
||||
nextNode.Parent = currNode;
|
||||
nextNode.state = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
nextNode.G = currNode.G + currNode.distances[i] + penalty;
|
||||
nextNode.F = nextNode.G + nextNode.H;
|
||||
nextNode.Parent = currNode;
|
||||
nextNode.state = 1;
|
||||
}
|
||||
//node that has been searched
|
||||
else if (nextNode.state == 1 || nextNode.state == -1)
|
||||
|
||||
float CalculateNodeCost()
|
||||
{
|
||||
float tempG = currNode.G + currNode.distances[i];
|
||||
|
||||
float penalty = 0f;
|
||||
if (GetNodePenalty != null)
|
||||
{
|
||||
float? nodePenalty = GetNodePenalty(currNode, nextNode);
|
||||
if (nodePenalty == null) { continue; }
|
||||
tempG += nodePenalty.Value;
|
||||
if (nodePenalty.HasValue)
|
||||
{
|
||||
penalty += nodePenalty.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return float.PositiveInfinity;
|
||||
}
|
||||
}
|
||||
|
||||
//only use if this new route is better than the
|
||||
//route the node was a part of
|
||||
if (tempG < nextNode.G)
|
||||
if (currNode.Waypoint.CurrentHull == null)
|
||||
{
|
||||
nextNode.G = tempG;
|
||||
nextNode.F = nextNode.G + nextNode.H;
|
||||
nextNode.Parent = currNode;
|
||||
nextNode.state = 1;
|
||||
penalty += outsideNodePenalty;
|
||||
}
|
||||
return currNode.G + currNode.distances[i] + penalty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -160,7 +160,8 @@ namespace Barotrauma
|
||||
aggregate += Items[i].Commonness;
|
||||
if (aggregate >= r && Items[i].Prefab != null)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
|
||||
//disabled to reduce the amount of data we collect through GA
|
||||
//GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
|
||||
Entity.Spawner?.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
|
||||
break;
|
||||
}
|
||||
@@ -293,10 +294,15 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CanPlayWith(Character player)
|
||||
{
|
||||
return AIController.Character.IsOnFriendlyTeam(player);
|
||||
}
|
||||
|
||||
public void Play(Character player)
|
||||
{
|
||||
if (PlayTimer > 0.0f) { return; }
|
||||
if (!AIController.Character.IsFriendly(player)) { return; }
|
||||
if (!CanPlayWith(player)) { return; }
|
||||
if (ToggleOwner)
|
||||
{
|
||||
Owner = Owner == player ? null : player;
|
||||
|
||||
@@ -608,7 +608,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!character.Inventory.IsInLimbSlot(item, i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand)) { continue; }
|
||||
#if DEBUG
|
||||
if (handlePos[i].LengthSquared() > ArmLength)
|
||||
if (ArmLength > 0 && handlePos[i].LengthSquared() > ArmLength)
|
||||
{
|
||||
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)",
|
||||
item.Prefab.ContentPackage);
|
||||
@@ -1041,8 +1041,16 @@ namespace Barotrauma
|
||||
Limb rightHand = GetLimb(LimbType.RightHand);
|
||||
if (rightHand == null) { return; }
|
||||
|
||||
rightShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.RightArm) ?? GetJoint(LimbType.RightArm, new LimbType[] { LimbType.RightHand, LimbType.RightForearm });
|
||||
leftShoulder = GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm) ?? GetJointBetweenLimbs(LimbType.Head, LimbType.LeftArm) ?? GetJoint(LimbType.LeftArm, new LimbType[] { LimbType.LeftHand, LimbType.LeftForearm });
|
||||
rightShoulder =
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.RightArm) ??
|
||||
GetJointBetweenLimbs(LimbType.Head, LimbType.RightArm) ??
|
||||
GetJoint(LimbType.RightArm, new LimbType[] { LimbType.RightHand, LimbType.RightForearm }) ??
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.RightHand);
|
||||
leftShoulder =
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftArm) ??
|
||||
GetJointBetweenLimbs(LimbType.Head, LimbType.LeftArm) ??
|
||||
GetJoint(LimbType.LeftArm, new LimbType[] { LimbType.LeftHand, LimbType.LeftForearm }) ??
|
||||
GetJointBetweenLimbs(LimbType.Torso, LimbType.LeftHand);
|
||||
|
||||
Vector2 localAnchorShoulder = Vector2.Zero;
|
||||
Vector2 localAnchorElbow = Vector2.Zero;
|
||||
|
||||
+1
-1
@@ -1561,7 +1561,7 @@ namespace Barotrauma
|
||||
string errorMsg =
|
||||
$"Attempted to move the anchor B of a limb's pull joint extremely far from the limb in {nameof(DragCharacter)}. " +
|
||||
$"Character in sub: {character.Submarine != null}, target in sub: {target.Submarine != null}.";
|
||||
GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Warning, errorMsg);
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
#endif
|
||||
|
||||
@@ -831,9 +831,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.DisableImpactDamageTimer > 0.0f) { return; }
|
||||
|
||||
if (f2.Body?.UserData is Item)
|
||||
if (f2.Body?.UserData is Item &&
|
||||
f2.Body.BodyType != BodyType.Static)
|
||||
{
|
||||
//no impact damage from items
|
||||
//no impact damage from items with a non-static body
|
||||
//items that can impact characters (melee weapons, projectiles) should handle the damage themselves
|
||||
return;
|
||||
}
|
||||
@@ -1092,7 +1093,7 @@ namespace Barotrauma
|
||||
Vector2 moveDir = hullDiff.LengthSquared() < 0.001f ? Vector2.UnitY : Vector2.Normalize(hullDiff);
|
||||
|
||||
//find a position 32 units away from the hull
|
||||
if (MathUtils.GetLineRectangleIntersection(
|
||||
if (MathUtils.GetLineWorldRectangleIntersection(
|
||||
newHull.WorldPosition,
|
||||
newHull.WorldPosition + moveDir * Math.Max(newHull.Rect.Width, newHull.Rect.Height),
|
||||
new Rectangle(newHull.WorldRect.X - 32, newHull.WorldRect.Y + 32, newHull.WorldRect.Width + 64, newHull.Rect.Height + 64),
|
||||
|
||||
@@ -225,7 +225,12 @@ namespace Barotrauma
|
||||
public float ImpactMultiplier { get; set; } = 1;
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
public float LevelWallDamage { get; set; }
|
||||
public float LevelWallDamage
|
||||
{
|
||||
get => _levelWallDamage * DamageMultiplier;
|
||||
set => _levelWallDamage = value;
|
||||
}
|
||||
private float _levelWallDamage;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable]
|
||||
public bool Ranged { get; set; }
|
||||
@@ -439,10 +444,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
//if level wall damage is not defined, default to the structure damage
|
||||
if (element.GetAttribute("LevelWallDamage") == null &&
|
||||
element.GetAttribute("levelwalldamage") == null)
|
||||
if (element.GetAttribute("LevelWallDamage") == null)
|
||||
{
|
||||
LevelWallDamage = StructureDamage;
|
||||
LevelWallDamage = _structureDamage;
|
||||
}
|
||||
|
||||
InitProjSpecific(element);
|
||||
|
||||
@@ -147,13 +147,23 @@ namespace Barotrauma
|
||||
public bool IsRemotePlayer { get; set; }
|
||||
|
||||
public bool IsLocalPlayer => Controlled == this;
|
||||
public bool IsPlayer => Controlled == this || IsRemotePlayer;
|
||||
|
||||
public bool IsPlayer => IsLocalPlayer || IsRemotePlayer;
|
||||
|
||||
/// <summary>
|
||||
/// Is the character player or does it have an active ship command manager (an AI controlled sub)? Bots in the player team are not treated as commanders.
|
||||
/// </summary>
|
||||
public bool IsCommanding => IsPlayer || AIController is HumanAIController { ShipCommandManager.Active: true };
|
||||
|
||||
/// <summary>
|
||||
/// Is the character actively controlled by a human AI?
|
||||
/// </summary>
|
||||
public bool IsBot => !IsPlayer && AIController is HumanAIController { Enabled: true };
|
||||
|
||||
/// <summary>
|
||||
/// Is the character actively controlled by an AI?
|
||||
/// </summary>
|
||||
public bool IsAIControlled => !IsPlayer && AIController is { Enabled: true };
|
||||
public bool IsEscorted { get; set; }
|
||||
public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
|
||||
|
||||
@@ -381,11 +391,11 @@ namespace Barotrauma
|
||||
|
||||
public bool IsOnPlayerTeam =>
|
||||
teamID == CharacterTeamType.Team1 ||
|
||||
(teamID == CharacterTeamType.Team2 && !IsFriendlyNPCTurnedHostile);
|
||||
(teamID == CharacterTeamType.Team2 && !IsFriendlyNPCTurnedHostile); // Some events use Team2 as a hostile team for NPCs, so we shouldn't treat those to be in any player team. Normally Team2 means a player team in PvP.
|
||||
|
||||
public bool IsOriginallyOnPlayerTeam => originalTeamID == CharacterTeamType.Team1 || originalTeamID == CharacterTeamType.Team2;
|
||||
public bool IsOriginallyOnPlayerTeam => originalTeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2;
|
||||
|
||||
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && (teamID == CharacterTeamType.Team2 || teamID == CharacterTeamType.None);
|
||||
public bool IsFriendlyNPCTurnedHostile => originalTeamID == CharacterTeamType.FriendlyNPC && teamID is CharacterTeamType.Team2 or CharacterTeamType.None;
|
||||
|
||||
public bool IsInstigator => CombatAction is { IsInstigator: true };
|
||||
|
||||
@@ -398,6 +408,12 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool IsCriminal;
|
||||
|
||||
/// <summary>
|
||||
/// A flag for the guards to remember that the character has used weapons or tools offensively, so that they know to confiscate those.
|
||||
/// Intentionally not set from stealing or fleeing.
|
||||
/// </summary>
|
||||
public bool IsActingOffensively;
|
||||
|
||||
/// <summary>
|
||||
/// Set true only, if the character is turned hostile from an escort mission (See <see cref="EscortMission"/>).
|
||||
/// </summary>
|
||||
@@ -752,11 +768,24 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
if (value == selectedCharacter) { return; }
|
||||
if (selectedCharacter != null) { selectedCharacter.selectedBy = null; }
|
||||
if (selectedCharacter != null) { selectedCharacter.selectedBy = null; }
|
||||
selectedCharacter = value;
|
||||
if (selectedCharacter != null) {selectedCharacter.selectedBy = this; }
|
||||
if (selectedCharacter != null) { selectedCharacter.selectedBy = this; }
|
||||
#if CLIENT
|
||||
CharacterHealth.SetHealthBarVisibility(value == null);
|
||||
|
||||
if (IsLocalPlayer && !GUI.IsUltrawide && GUI.IsHUDScaled)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
// Scaled HUD on non-ultra-wide -> hide the chatbox, so that it doesn't overlap with the inventory.
|
||||
ChatBox.AutoHideChatBox();
|
||||
}
|
||||
else
|
||||
{
|
||||
ChatBox.ResetChatBoxOpenState();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
|
||||
CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value));
|
||||
@@ -937,10 +966,7 @@ namespace Barotrauma
|
||||
get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
|
||||
}
|
||||
|
||||
public bool IsPet
|
||||
{
|
||||
get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; }
|
||||
}
|
||||
public bool IsPet => Params.IsPet;
|
||||
|
||||
public float Oxygen
|
||||
{
|
||||
@@ -1085,18 +1111,18 @@ namespace Barotrauma
|
||||
}
|
||||
#if CLIENT
|
||||
HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem);
|
||||
if (Controlled == this)
|
||||
if (IsLocalPlayer)
|
||||
{
|
||||
_selectedItem?.GetComponent<Fabricator>()?.RefreshSelectedItem();
|
||||
|
||||
if (_selectedItem == null)
|
||||
{
|
||||
GameMain.GameSession?.CrewManager?.ResetCrewList();
|
||||
}
|
||||
else if (!_selectedItem.IsLadder)
|
||||
if (_selectedItem != null)
|
||||
{
|
||||
GameMain.GameSession?.CrewManager?.AutoHideCrewList();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.GameSession?.CrewManager?.ResetCrewListOpenState();
|
||||
}
|
||||
|
||||
_selectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
|
||||
}
|
||||
@@ -1199,7 +1225,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsKnockedDown && (!IsRagdolled || AnimController.IsHoldingToRope);
|
||||
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsKnockedDownOrRagdolled && (!IsRagdolled || AnimController.IsHoldingToRope);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,12 +1456,9 @@ namespace Barotrauma
|
||||
//no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet)
|
||||
Info.IsNewHire = false;
|
||||
}
|
||||
if (characterInfo?.HumanPrefabIds is { } prefabIds &&
|
||||
prefabIds.NpcSetIdentifier != default && prefabIds.NpcIdentifier != default)
|
||||
if (characterInfo?.HumanPrefabIds is { NpcSetIdentifier.IsEmpty: false, NpcIdentifier.IsEmpty: false })
|
||||
{
|
||||
HumanPrefab = NPCSet.Get(
|
||||
characterInfo.HumanPrefabIds.NpcSetIdentifier,
|
||||
characterInfo.HumanPrefabIds.NpcIdentifier);
|
||||
HumanPrefab = characterInfo.HumanPrefab;
|
||||
}
|
||||
|
||||
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
|
||||
@@ -1831,12 +1854,12 @@ namespace Barotrauma
|
||||
if (info == null) { return; }
|
||||
if (info.HumanPrefabIds != default)
|
||||
{
|
||||
var humanPrefab = NPCSet.Get(info.HumanPrefabIds.NpcSetIdentifier, info.HumanPrefabIds.NpcIdentifier);
|
||||
if (humanPrefab == null)
|
||||
var prefab = info.HumanPrefab;
|
||||
if (prefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to give job items for the character \"{Name}\" - could not find human prefab with the id \"{info.HumanPrefabIds.NpcIdentifier}\" from \"{info.HumanPrefabIds.NpcSetIdentifier}\".");
|
||||
}
|
||||
else if (humanPrefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
|
||||
else if (prefab.GiveItems(this, spawnPoint?.Submarine ?? Submarine, spawnPoint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1905,23 +1928,9 @@ namespace Barotrauma
|
||||
|
||||
if (skillIdentifier != null)
|
||||
{
|
||||
foreach (Item item in Inventory.AllItems)
|
||||
if (wearableSkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
|
||||
{
|
||||
if (item?.GetComponent<Wearable>() is Wearable wearable &&
|
||||
!Inventory.IsInLimbSlot(item, InvSlotType.Any))
|
||||
{
|
||||
foreach (var allowedSlot in wearable.AllowedSlots)
|
||||
{
|
||||
if (allowedSlot == InvSlotType.Any) { continue; }
|
||||
if (!Inventory.IsInLimbSlot(item, allowedSlot)) { continue; }
|
||||
if (wearable.SkillModifiers.TryGetValue(skillIdentifier, out float skillValue))
|
||||
{
|
||||
skillLevel += skillValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
skillLevel += skillValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2667,7 +2676,7 @@ namespace Barotrauma
|
||||
public bool CanBeDraggedBy(Character character)
|
||||
{
|
||||
if (!IsDraggable) { return false; }
|
||||
return IsKnockedDown || LockHands || (IsPet && character.IsFriendly(this)) || (IsBot && character.TeamID == TeamID);
|
||||
return IsKnockedDownOrRagdolled || LockHands || (IsPet && character.IsOnFriendlyTeam(this)) || (IsBot && character.TeamID == TeamID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3051,12 +3060,7 @@ namespace Barotrauma
|
||||
|
||||
public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
|
||||
{
|
||||
bool isLocalPlayer = Controlled == this;
|
||||
|
||||
if (!isLocalPlayer && (this is AICharacter && !IsRemotePlayer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (IsAIControlled) { return; }
|
||||
|
||||
if (DisableInteract)
|
||||
{
|
||||
@@ -3077,7 +3081,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (isLocalPlayer)
|
||||
if (IsLocalPlayer)
|
||||
{
|
||||
if (!IsMouseOnUI && (ViewTarget == null || ViewTarget == this) && !DisableFocusingOnEntities)
|
||||
{
|
||||
@@ -3597,23 +3601,25 @@ namespace Barotrauma
|
||||
humanAnimController.Crouching = false;
|
||||
}
|
||||
//ragdolling manually makes the character go through platforms
|
||||
//EXCEPT for clients, they rely on the server telling whether platforms should be ignored or not
|
||||
if (IsRagdolled && GameMain.NetworkMember is not { IsClient: true })
|
||||
//EXCEPT if the character is controlled by the server (i.e. remote player or bot),
|
||||
//in that case the server decides whether platforms should be ignored or not
|
||||
bool isControlledByRemotelyByServer = GameMain.NetworkMember is { IsClient: true } && IsRemotelyControlled;
|
||||
if (IsRagdolled &&
|
||||
!isControlledByRemotelyByServer)
|
||||
{
|
||||
AnimController.IgnorePlatforms = true;
|
||||
}
|
||||
AnimController.ResetPullJoints();
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
SelectedCharacter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
//AI and control stuff
|
||||
|
||||
Control(deltaTime, cam);
|
||||
|
||||
bool isNotControlled = Controlled != this;
|
||||
|
||||
if (isNotControlled && (!(this is AICharacter) || IsRemotePlayer))
|
||||
|
||||
if (IsRemotePlayer)
|
||||
{
|
||||
Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
|
||||
DoInteractionUpdate(deltaTime, mouseSimPos);
|
||||
@@ -3880,7 +3886,9 @@ namespace Barotrauma
|
||||
|
||||
//don't spawn duffel bags in PvP modes that include respawning, because it can lead to a ton of accumulated items in the sub/outpost
|
||||
bool pvpWithRespawning = GameMain.GameSession?.GameMode is PvPMode && GameMain.NetworkMember?.RespawnManager != null;
|
||||
if (!despawnContainerId.IsEmpty && !pvpWithRespawning)
|
||||
if (!despawnContainerId.IsEmpty && !pvpWithRespawning &&
|
||||
//don't duffelbag disconnected character's items (the items should disappear with the character, and reappear if they rejoin later)
|
||||
CauseOfDeath?.Type != CauseOfDeathType.Disconnected)
|
||||
{
|
||||
var containerPrefab =
|
||||
MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
|
||||
@@ -3928,6 +3936,16 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
#if SERVER
|
||||
if (CauseOfDeath?.Type == CauseOfDeathType.Disconnected)
|
||||
{
|
||||
if (GameMain.GameSession?.GameMode is MultiPlayerCampaign mpCampaign)
|
||||
{
|
||||
//refresh campaign data so the disconnected player gets to keep the items that were in their inventory
|
||||
mpCampaign.RefreshCharacterCampaignData(this, refreshHealthData: false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Spawner.AddEntityToRemoveQueue(this);
|
||||
}
|
||||
}
|
||||
@@ -4720,17 +4738,24 @@ namespace Barotrauma
|
||||
healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool IsKnockedDownOrRagdolled => (IsRagdolled && !AnimController.IsHangingWithRope) || IsKnockedDown;
|
||||
|
||||
/// <summary>
|
||||
/// Is the character knocked down regardless whether the technical state is dead, unconcious, paralyzed, or stunned.
|
||||
/// With stunning, the parameter uses an one second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
|
||||
/// With stunning, the parameter uses a one-second delay before the character is treated as knocked down. The purpose of this is to ignore minor stunning. If you don't want to to ignore any stun, use the Stun property.
|
||||
/// </summary>
|
||||
public bool IsKnockedDown => (IsRagdolled && !AnimController.IsHangingWithRope) || CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
|
||||
public bool IsKnockedDown => CharacterHealth.StunTimer > 1.0f || IsIncapacitated;
|
||||
|
||||
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
|
||||
{
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
|
||||
if (Screen.Selected != GameMain.GameScreen) { return; }
|
||||
if (GodMode)
|
||||
{
|
||||
CharacterHealth.Stun = 0;
|
||||
return;
|
||||
}
|
||||
if (newStun > 0 && Params.Health.StunImmunity)
|
||||
{
|
||||
if (EmpVulnerability <= 0 || CharacterHealth.GetAfflictionStrengthByType(AfflictionPrefab.EMPType, allowLimbAfflictions: false) <= 0)
|
||||
@@ -5008,6 +5033,8 @@ namespace Barotrauma
|
||||
{
|
||||
characterInfo.PermanentlyDead = true;
|
||||
}
|
||||
|
||||
GameMain.GameSession.RefreshAnyOpenPlayerInfo();
|
||||
#endif
|
||||
|
||||
#if SERVER
|
||||
@@ -5016,8 +5043,10 @@ namespace Barotrauma
|
||||
Info.LastRewardDistribution = Option.Some(Wallet.RewardDistribution);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage)
|
||||
//we don't need info of every kill, we can get a good sample size just by logging 5%
|
||||
if (GameAnalyticsManager.SendUserStatistics &&
|
||||
Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage &&
|
||||
GameAnalyticsManager.ShouldLogRandomSample())
|
||||
{
|
||||
string causeOfDeathStr = causeOfDeathAffliction == null ?
|
||||
causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Identifier.Value.Replace(" ", "");
|
||||
@@ -5297,6 +5326,7 @@ namespace Barotrauma
|
||||
}
|
||||
#if SERVER
|
||||
newItem.GetComponent<Terminal>()?.SyncHistory();
|
||||
if (newItem.GetComponent<WifiComponent>() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); }
|
||||
if (newItem.GetComponent<GeneticMaterial>() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); }
|
||||
SyncInGameEditables(newItem);
|
||||
#endif
|
||||
@@ -5511,7 +5541,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public bool IsProtectedFromPressure => IsImmuneToPressure || PressureProtection >= (Level.Loaded?.GetRealWorldDepth(WorldPosition.Y) ?? 1.0f);
|
||||
|
||||
public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure);
|
||||
public bool IsImmuneToPressure => !NeedsAir || HasAbilityFlag(AbilityFlags.ImmuneToPressure) || GodMode;
|
||||
|
||||
#region Talents
|
||||
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
|
||||
@@ -5772,9 +5802,14 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private readonly Dictionary<StatTypes, float> wearableStatValues = new Dictionary<StatTypes, float>();
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary with temporary values, updated when the character equips/unequips wearables. Used to reduce unnecessary inventory checking.
|
||||
/// </summary>
|
||||
private readonly Dictionary<Identifier, float> wearableSkillModifiers = new Dictionary<Identifier, float>();
|
||||
|
||||
public float GetStatValue(StatTypes statType, bool includeSaved = true)
|
||||
{
|
||||
if (!IsHuman) { return 0f; }
|
||||
if (Info == null) { return 0f; }
|
||||
|
||||
float statValue = 0f;
|
||||
if (statValues.TryGetValue(statType, out float value))
|
||||
@@ -5785,7 +5820,7 @@ namespace Barotrauma
|
||||
{
|
||||
statValue += CharacterHealth.GetStatValue(statType);
|
||||
}
|
||||
if (Info != null && includeSaved)
|
||||
if (includeSaved)
|
||||
{
|
||||
// could be optimized by instead updating the Character.cs statvalues dictionary whenever the CharacterInfo.cs values change
|
||||
statValue += Info.GetSavedStatValue(statType);
|
||||
@@ -5807,12 +5842,17 @@ namespace Barotrauma
|
||||
|
||||
public void OnWearablesChanged()
|
||||
{
|
||||
HashSet<Wearable> handledWearables = new HashSet<Wearable>();
|
||||
wearableStatValues.Clear();
|
||||
wearableSkillModifiers.Clear();
|
||||
for (int i = 0; i < Inventory.Capacity; i++)
|
||||
{
|
||||
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.SlotTypes[i] != InvSlotType.LeftHand && Inventory.SlotTypes[i] != InvSlotType.RightHand
|
||||
&& Inventory.GetItemAt(i)?.GetComponent<Wearable>() is Wearable wearable)
|
||||
{
|
||||
if (handledWearables.Contains(wearable)) { continue; }
|
||||
handledWearables.Add(wearable);
|
||||
|
||||
foreach (var statValuePair in wearable.WearableStatValues)
|
||||
{
|
||||
if (wearableStatValues.ContainsKey(statValuePair.Key))
|
||||
@@ -5824,6 +5864,17 @@ namespace Barotrauma
|
||||
wearableStatValues.Add(statValuePair.Key, statValuePair.Value);
|
||||
}
|
||||
}
|
||||
foreach (var skillModifier in wearable.SkillModifiers)
|
||||
{
|
||||
if (wearableSkillModifiers.ContainsKey(skillModifier.Key))
|
||||
{
|
||||
wearableSkillModifiers[skillModifier.Key] += skillModifier.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
wearableSkillModifiers.Add(skillModifier.Key, skillModifier.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +301,25 @@ namespace Barotrauma
|
||||
public bool PermanentlyDead;
|
||||
public bool RenamingEnabled = false;
|
||||
|
||||
private BotStatus botStatus = BotStatus.ActiveService;
|
||||
|
||||
public BotStatus BotStatus
|
||||
{
|
||||
get => botStatus;
|
||||
set
|
||||
{
|
||||
botStatus = value;
|
||||
if (botStatus == BotStatus.ActiveService && character == null)
|
||||
{
|
||||
//no character yet -> spawn is pending
|
||||
PendingSpawnToActiveService = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOnReserveBench => BotStatus == BotStatus.ReserveBench;
|
||||
public bool PendingSpawnToActiveService;
|
||||
|
||||
private static ushort idCounter = 1;
|
||||
private const string disguiseName = "???";
|
||||
|
||||
@@ -312,7 +331,18 @@ namespace Barotrauma
|
||||
public LocalizedString Title;
|
||||
|
||||
public (Identifier NpcSetIdentifier, Identifier NpcIdentifier) HumanPrefabIds;
|
||||
|
||||
|
||||
private HumanPrefab _humanPrefab;
|
||||
public HumanPrefab HumanPrefab
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HumanPrefabIds == default) { return null; }
|
||||
_humanPrefab ??= NPCSet.Get(HumanPrefabIds.NpcSetIdentifier, HumanPrefabIds.NpcIdentifier);
|
||||
return _humanPrefab;
|
||||
}
|
||||
}
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get
|
||||
@@ -340,10 +370,24 @@ namespace Barotrauma
|
||||
|
||||
public Identifier SpeciesName { get; }
|
||||
|
||||
private Character character;
|
||||
/// <summary>
|
||||
/// Note: Can be null.
|
||||
/// </summary>
|
||||
public Character Character;
|
||||
public Character Character
|
||||
{
|
||||
get => character;
|
||||
set
|
||||
{
|
||||
character = value;
|
||||
if (character != null)
|
||||
{
|
||||
//character spawned -> spawn no longer pending
|
||||
PendingSpawnToActiveService = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Job Job;
|
||||
|
||||
@@ -768,6 +812,13 @@ namespace Barotrauma
|
||||
return name;
|
||||
}
|
||||
|
||||
public void SetNameBasedOnJob()
|
||||
{
|
||||
if (Job == null) { return; }
|
||||
Name = Job.Name.Value;
|
||||
OriginalName = Name;
|
||||
}
|
||||
|
||||
public static Color SelectRandomColor(in ImmutableArray<(Color Color, float Commonness)> array, Rand.RandSync randSync)
|
||||
=> ToolBox.SelectWeightedRandom(array, array.Select(p => p.Commonness).ToArray(), randSync)
|
||||
.Color;
|
||||
@@ -830,6 +881,7 @@ namespace Barotrauma
|
||||
LoadTagsBackwardsCompatibility(infoElement, tags);
|
||||
SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
|
||||
PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false);
|
||||
BotStatus = infoElement.GetAttributeBool(nameof(IsOnReserveBench), false) ? BotStatus.ReserveBench : BotStatus.ActiveService;
|
||||
RenamingEnabled = infoElement.GetAttributeBool("renamingenabled", false);
|
||||
ContentXElement element;
|
||||
if (!SpeciesName.IsEmpty)
|
||||
@@ -1570,6 +1622,7 @@ namespace Barotrauma
|
||||
new XAttribute("refundpoints", TalentRefundPoints),
|
||||
new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
|
||||
new XAttribute("permanentlydead", PermanentlyDead),
|
||||
new XAttribute(nameof(IsOnReserveBench), IsOnReserveBench),
|
||||
new XAttribute("renamingenabled", RenamingEnabled)
|
||||
);
|
||||
|
||||
|
||||
+20
-1
@@ -54,6 +54,11 @@ namespace Barotrauma
|
||||
activeEffectDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Armor penetration for status effects. Normally defined per attack, but that doesn't work on status effects.
|
||||
/// </summary>
|
||||
public float Penetration { get; set; }
|
||||
|
||||
private float _nonClampedStrength = -1;
|
||||
public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength;
|
||||
@@ -64,7 +69,7 @@ namespace Barotrauma
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The probability for the affliction to be applied."), Editable(minValue: 0f, maxValue: 1f)]
|
||||
public float Probability { get; set; } = 1.0f;
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects explosions."), Editable]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Explosion damage is applied per each affected limb. Should this affliction damage be divided by the count of affected limbs (1-15) or applied in full? Default: true. Only affects status effects and explosions."), Editable]
|
||||
public bool DivideByLimbCount { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Is the damage relative to the max vitality (percentage) or absolute (normal)"), Editable]
|
||||
@@ -120,6 +125,7 @@ namespace Barotrauma
|
||||
Probability = source.Probability;
|
||||
DivideByLimbCount = source.DivideByLimbCount;
|
||||
MultiplyByMaxVitality = source.MultiplyByMaxVitality;
|
||||
Penetration = source.Penetration;
|
||||
}
|
||||
|
||||
public void Serialize(XElement element)
|
||||
@@ -408,6 +414,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
//force an update when a periodic effect triggers to get it to trigger client-side
|
||||
characterHealth.Character.healthUpdateTimer = 0.0f;
|
||||
foreach (StatusEffect statusEffect in periodicEffect.StatusEffects)
|
||||
{
|
||||
ApplyStatusEffect(ActionType.OnActive, statusEffect, 1.0f, characterHealth, targetLimb);
|
||||
@@ -447,6 +455,17 @@ namespace Barotrauma
|
||||
ApplyStatusEffect(ActionType.OnActive, statusEffect, deltaTime, characterHealth, targetLimb);
|
||||
}
|
||||
|
||||
if (currentEffect.ConvulseAmount > 0f)
|
||||
{
|
||||
foreach (Limb limb in characterHealth.Character.AnimController.Limbs)
|
||||
{
|
||||
if (limb.IsSevered) { continue; }
|
||||
if (limb.Hidden) { continue; }
|
||||
float force = Rand.Value() * limb.Mass * currentEffect.ConvulseAmount;
|
||||
limb.body.ApplyLinearImpulse(Rand.Vector(force), maxVelocity: Networking.NetConfig.MaxPhysicsBodyVelocity * 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
float amount = deltaTime;
|
||||
if (Prefab.GrainBurst > 0)
|
||||
{
|
||||
|
||||
+28
-20
@@ -321,6 +321,10 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var husk = Character.Create(huskedSpeciesName, character.WorldPosition, ToolBox.RandomSeed(8), huskCharacterInfo, isRemotePlayer: false, hasAi: true);
|
||||
if (character.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
|
||||
{
|
||||
husk.AddAbilityFlag(AbilityFlags.IgnoredByEnemyAI);
|
||||
}
|
||||
if (husk.Info != null)
|
||||
{
|
||||
husk.Info.Character = husk;
|
||||
@@ -395,13 +399,13 @@ namespace Barotrauma
|
||||
|
||||
public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, Identifier huskedSpeciesName, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
|
||||
{
|
||||
var appendage = new List<Limb>();
|
||||
var appendageLimbs = new List<Limb>();
|
||||
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
|
||||
if (huskPrefab?.ConfigElement == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!",
|
||||
contentPackage: matchingAffliction.ContentPackage);
|
||||
return appendage;
|
||||
return appendageLimbs;
|
||||
}
|
||||
var mainElement = huskPrefab.ConfigElement;
|
||||
var element = appendageDefinition;
|
||||
@@ -413,11 +417,11 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{matchingAffliction.Identifier}'!",
|
||||
contentPackage: matchingAffliction.ContentPackage);
|
||||
return appendage;
|
||||
return appendageLimbs;
|
||||
}
|
||||
ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
|
||||
XDocument doc = XMLExtensions.TryLoadXml(pathToAppendage);
|
||||
if (doc == null) { return appendage; }
|
||||
if (doc == null) { return appendageLimbs; }
|
||||
ragdoll ??= character.AnimController;
|
||||
if (ragdoll.Dir < 1.0f)
|
||||
{
|
||||
@@ -425,13 +429,13 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var root = doc.Root.FromPackage(pathToAppendage.ContentPackage);
|
||||
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
|
||||
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeInt("id", -1), e => e);
|
||||
//the IDs may need to be offset if the character has other extra appendages (e.g. from gene splicing)
|
||||
//that take up the IDs of this appendage
|
||||
int idOffset = 0;
|
||||
int? idOffset = null;
|
||||
foreach (var jointElement in root.GetChildElements("joint"))
|
||||
{
|
||||
if (!limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { continue; }
|
||||
if (!limbElements.TryGetValue(jointElement.GetAttributeInt("limb2", -1), out ContentXElement limbElement)) { continue; }
|
||||
|
||||
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
|
||||
Limb attachLimb = null;
|
||||
@@ -453,28 +457,32 @@ namespace Barotrauma
|
||||
}
|
||||
if (attachLimb != null)
|
||||
{
|
||||
jointParams.Limb1 = attachLimb.Params.ID;
|
||||
//the joint attaches to a limb outside the character's normal limb count = to another part of the appendage
|
||||
// -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
|
||||
if (jointParams.Limb1 >= ragdoll.RagdollParams.Limbs.Count)
|
||||
{
|
||||
jointParams.Limb1 += idOffset;
|
||||
}
|
||||
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams);
|
||||
if (idOffset == 0)
|
||||
idOffset ??= ragdoll.Limbs.Length - appendageLimbParams.ID;
|
||||
jointParams.Limb1 = attachLimb.Params.ID;
|
||||
//the joint attaches to one of the limbs we're creating = to another part of the appendage
|
||||
// -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
|
||||
if (limbElements.ContainsKey(jointParams.Limb1))
|
||||
{
|
||||
idOffset = ragdoll.Limbs.Length - appendageLimbParams.ID;
|
||||
jointParams.Limb1 += idOffset.Value;
|
||||
}
|
||||
jointParams.Limb2 = appendageLimbParams.ID = ragdoll.Limbs.Length;
|
||||
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
|
||||
if (limbElements.ContainsKey(jointParams.Limb2))
|
||||
{
|
||||
jointParams.Limb2 += idOffset.Value;
|
||||
}
|
||||
Limb huskAppendage =
|
||||
//check if this joint is supposed to attach to a limb we already created
|
||||
appendageLimbs.Find(limb => limb.Params.ID == appendageLimbParams.ID) ??
|
||||
//if not, create a new limb
|
||||
new Limb(ragdoll, character, appendageLimbParams);
|
||||
huskAppendage.body.Submarine = character.Submarine;
|
||||
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
|
||||
ragdoll.AddLimb(huskAppendage);
|
||||
ragdoll.AddJoint(jointParams);
|
||||
appendage.Add(huskAppendage);
|
||||
appendageLimbs.Add(huskAppendage);
|
||||
}
|
||||
}
|
||||
return appendage;
|
||||
return appendageLimbs;
|
||||
}
|
||||
|
||||
public static Identifier GetHuskedSpeciesName(CharacterParams character, AfflictionPrefabHusk prefab)
|
||||
|
||||
+18
@@ -372,6 +372,10 @@ namespace Barotrauma
|
||||
description: $"Color of the \"thermal goggles overlay\" enabled by the affliction. Only has an effect if {nameof(ThermalOverlayRange)} is larger than 0.")]
|
||||
public Color ThermalOverlayColor { get; private set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.No,
|
||||
description: "Multiplier for the convulsion/seizure effect on the character's ragdoll when this effect is active.")]
|
||||
public float ConvulseAmount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// StatType that will be applied to the affected character when the effect is active that is proportional to the effect's strength.
|
||||
/// </summary>
|
||||
@@ -641,6 +645,7 @@ namespace Barotrauma
|
||||
public static AfflictionPrefab Stun => Prefabs[StunType];
|
||||
public static AfflictionPrefab RadiationSickness => Prefabs["radiationsickness"];
|
||||
public static AfflictionPrefab HuskInfection => Prefabs["huskinfection"];
|
||||
public static AfflictionPrefab JovianRadiation => Prefabs["jovianradiation"];
|
||||
|
||||
public static readonly PrefabCollection<AfflictionPrefab> Prefabs = new PrefabCollection<AfflictionPrefab>();
|
||||
|
||||
@@ -657,6 +662,11 @@ namespace Barotrauma
|
||||
private readonly LocalizedString defaultDescription;
|
||||
public readonly ImmutableList<Description> Descriptions;
|
||||
|
||||
/// <summary>
|
||||
/// Should the affliction's description be included in the tooltips on the affliction icons above the health bar?
|
||||
/// </summary>
|
||||
public readonly bool ShowDescriptionInTooltip;
|
||||
|
||||
/// <summary>
|
||||
/// Arbitrary string that is used to identify the type of the affliction.
|
||||
/// </summary>
|
||||
@@ -902,6 +912,8 @@ namespace Barotrauma
|
||||
{
|
||||
defaultDescription = defaultDescription.Fallback(fallbackDescription);
|
||||
}
|
||||
ShowDescriptionInTooltip = element.GetAttributeBool(nameof(ShowDescriptionInTooltip), true);
|
||||
|
||||
IsBuff = element.GetAttributeBool(nameof(IsBuff), false);
|
||||
AffectMachines = element.GetAttributeBool(nameof(AffectMachines), true);
|
||||
|
||||
@@ -939,6 +951,12 @@ namespace Barotrauma
|
||||
HideIconAfterDelay = element.GetAttributeBool(nameof(HideIconAfterDelay), false);
|
||||
|
||||
ActivationThreshold = element.GetAttributeFloat(nameof(ActivationThreshold), 0.0f);
|
||||
if (Identifier == StunType && ActivationThreshold > 0.0f)
|
||||
{
|
||||
ActivationThreshold = 0.0f;
|
||||
DebugConsole.AddWarning($"Error in affliction prefab {Identifier}: activation threshold of the stun affliction must be 0, because the strength of the affliction represents the length of the stun and any amount of stun has an effect.");
|
||||
}
|
||||
|
||||
ShowIconThreshold = element.GetAttributeFloat(nameof(ShowIconThreshold), Math.Max(ActivationThreshold, 0.05f));
|
||||
ShowIconToOthersThreshold = element.GetAttributeFloat(nameof(ShowIconToOthersThreshold), ShowIconThreshold);
|
||||
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
|
||||
|
||||
@@ -736,6 +736,8 @@ namespace Barotrauma
|
||||
afflictionsToRemove.AddRange(afflictions.Keys.Where(a => !irremovableAfflictions.Contains(a)));
|
||||
foreach (var affliction in afflictionsToRemove)
|
||||
{
|
||||
//set strength to 0 in case the affliction needs to react to becoming inactive
|
||||
affliction.Strength = 0.0f;
|
||||
afflictions.Remove(affliction);
|
||||
}
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
@@ -902,6 +904,8 @@ namespace Barotrauma
|
||||
affliction.Duration -= deltaTime;
|
||||
if (affliction.Duration <= 0.0f)
|
||||
{
|
||||
//set strength to 0 in case the affliction needs to react to becoming inactive
|
||||
affliction.Strength = 0.0f;
|
||||
afflictionsToRemove.Add(affliction);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -251,7 +252,7 @@ namespace Barotrauma
|
||||
foreach (var skill in characterInfo.Job.GetSkills())
|
||||
{
|
||||
float newSkill = skill.Level * SkillMultiplier;
|
||||
skill.IncreaseSkill(newSkill - skill.Level, increasePastMax: false);
|
||||
skill.IncreaseSkill(newSkill - skill.Level, canIncreasePastDefaultMaximumSkill: false);
|
||||
}
|
||||
}
|
||||
characterInfo.Salary = characterInfo.CalculateSalary(BaseSalary, SalaryMultiplier);
|
||||
@@ -259,6 +260,12 @@ namespace Barotrauma
|
||||
characterInfo.GiveExperience(ExperiencePoints);
|
||||
return characterInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Items marked to be spawned infinitely (by NPCs).
|
||||
/// </summary>
|
||||
private readonly Dictionary<Identifier, ItemPrefab> infiniteItems = new();
|
||||
public IReadOnlyCollection<ItemPrefab> InfiniteItems => infiniteItems.Values;
|
||||
|
||||
public static void InitializeItem(Character character, ContentXElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true)
|
||||
{
|
||||
@@ -323,9 +330,13 @@ namespace Barotrauma
|
||||
wifiComponent.TeamID = character.TeamID;
|
||||
}
|
||||
parentItem?.Combine(item, user: null);
|
||||
if (itemElement.GetAttributeBool(nameof(JobPrefab.JobItem.Infinite), false))
|
||||
{
|
||||
humanPrefab.infiniteItems.TryAdd(itemPrefab.Identifier, itemPrefab);
|
||||
}
|
||||
foreach (ContentXElement childItemElement in itemElement.Elements())
|
||||
{
|
||||
int amount = childItemElement.GetAttributeInt("amount", 1);
|
||||
int amount = childItemElement.GetAttributeInt(nameof(JobPrefab.JobItem.Amount), 1);
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
InitializeItem(character, childItemElement, submarine, humanPrefab, spawnPoint, item, createNetworkEvents);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
@@ -127,6 +128,11 @@ namespace Barotrauma
|
||||
new Skill(skillIdentifier, increase));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Does not automatically filter items by team or by game mode. See <see cref="JobItem.GetItemIdentifier(CharacterTeamType?, JobItem.GameModeType)"/>
|
||||
/// </summary>
|
||||
public bool HasJobItem(Func<JobPrefab.JobItem, bool> predicate) => prefab.HasJobItem(Variant, predicate);
|
||||
|
||||
public void GiveJobItems(Character character, bool isPvPMode, WayPoint spawnPoint = null)
|
||||
{
|
||||
|
||||
@@ -94,6 +94,18 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Does not automatically filter items by team or by game mode. See <see cref="JobItem.GetItemIdentifier(CharacterTeamType, bool)"/>
|
||||
/// </summary>
|
||||
public IEnumerable<JobItem> GetJobItems(int jobVariant, Func<JobItem, bool> predicate)
|
||||
=> JobItems.TryGetValue(jobVariant, out ImmutableArray<JobItem> items) ? items.Where(predicate) : Enumerable.Empty<JobItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Note: Does not automatically filter items by team or by game mode. See <see cref="JobItem.GetItemIdentifier(CharacterTeamType, bool)"/>
|
||||
/// </summary>
|
||||
public bool HasJobItem(int jobVariant, Func<JobItem, bool> predicate)
|
||||
=> JobItems.TryGetValue(jobVariant, out ImmutableArray<JobItem> items) && items.Any(predicate);
|
||||
|
||||
public class JobItem
|
||||
{
|
||||
@@ -107,7 +119,8 @@ namespace Barotrauma
|
||||
public readonly bool ShowPreview;
|
||||
public readonly bool Equip;
|
||||
public readonly bool Outfit;
|
||||
public readonly int Amount = 1;
|
||||
public readonly int Amount;
|
||||
public readonly bool Infinite;
|
||||
|
||||
public readonly JobItem ParentItem;
|
||||
|
||||
@@ -117,14 +130,15 @@ namespace Barotrauma
|
||||
{
|
||||
ItemIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
ItemIdentifierTeam2 = element.GetAttributeIdentifier("identifierteam2", Identifier.Empty);
|
||||
ShowPreview = element.GetAttributeBool("showpreview", true);
|
||||
GameMode = element.GetAttributeEnum("gamemode", parentItem?.GameMode ?? GameModeType.Any);
|
||||
Amount = element.GetAttributeInt("amount", 1);
|
||||
Equip = element.GetAttributeBool("equip", false);
|
||||
Outfit = element.GetAttributeBool("outfit", false);
|
||||
ShowPreview = element.GetAttributeBool(nameof(ShowPreview), true);
|
||||
GameMode = element.GetAttributeEnum(nameof(GameMode), parentItem?.GameMode ?? GameModeType.Any);
|
||||
Amount = element.GetAttributeInt(nameof(Amount), 1);
|
||||
Equip = element.GetAttributeBool(nameof(Equip), false);
|
||||
Outfit = element.GetAttributeBool(nameof(Outfit), false);
|
||||
Infinite = element.GetAttributeBool(nameof(Infinite), false);
|
||||
ParentItem = parentItem;
|
||||
}
|
||||
|
||||
|
||||
public Identifier GetItemIdentifier(CharacterTeamType team, bool isPvPMode)
|
||||
{
|
||||
switch (GameMode)
|
||||
@@ -136,11 +150,10 @@ namespace Barotrauma
|
||||
if (isPvPMode) { return Identifier.Empty; }
|
||||
break;
|
||||
}
|
||||
|
||||
return
|
||||
team == CharacterTeamType.Team2 && !ItemIdentifierTeam2.IsEmpty ?
|
||||
ItemIdentifierTeam2 :
|
||||
ItemIdentifier;
|
||||
ItemIdentifierTeam2 :
|
||||
ItemIdentifier;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly Identifier Identifier;
|
||||
|
||||
public const float MaximumSkill = 100.0f;
|
||||
/// <summary>
|
||||
/// The "normal" maximum skill level. It's possible to go above this with certain talents, see <see cref="SkillSettings.MaximumSkillWithTalents"/>.
|
||||
/// </summary>
|
||||
public const float DefaultMaximumSkill = 100.0f;
|
||||
|
||||
private float level;
|
||||
|
||||
@@ -27,9 +30,21 @@ namespace Barotrauma
|
||||
|
||||
public LocalizedString DisplayName { get; private set; }
|
||||
|
||||
public void IncreaseSkill(float value, bool increasePastMax)
|
||||
/// <summary>
|
||||
/// Increase the skill level by a value. Handles clamping the level above the maximum.
|
||||
/// Note that if the skill level is already above maximum (if it for example has been set by console commands), it's allowed to stay at that level, but not to increase further.
|
||||
/// </summary>
|
||||
/// <param name="value">How much to increase the skill.</param>
|
||||
/// <param name="canIncreasePastDefaultMaximumSkill">Can the skill level increase above <see cref="DefaultMaximumSkill"/>, or can it go all the way to <see cref="SkillSettings.MaximumSkillWithTalents"/>?</param>
|
||||
public void IncreaseSkill(float value, bool canIncreasePastDefaultMaximumSkill)
|
||||
{
|
||||
Level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
|
||||
float currentMaximum = canIncreasePastDefaultMaximumSkill ? SkillSettings.Current.MaximumSkillWithTalents : DefaultMaximumSkill;
|
||||
if (Level > currentMaximum && value > 0)
|
||||
{
|
||||
//level above max already (set with console commands?), don't allow increasing it further and don't clamp it below max either
|
||||
return;
|
||||
}
|
||||
Level = MathHelper.Clamp(level + value, 0.0f, currentMaximum);
|
||||
}
|
||||
|
||||
private readonly Identifier iconJobId;
|
||||
|
||||
+7
-5
@@ -182,7 +182,7 @@ namespace Barotrauma
|
||||
public float HandIKStrength { get; set; }
|
||||
|
||||
public static string GetDefaultFileName(Identifier speciesName, AnimationType animType) => $"{speciesName.Value.CapitaliseFirstInvariant()}{animType}";
|
||||
public static string GetDefaultFile(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
|
||||
public static string GetDefaultFilePath(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
|
||||
|
||||
public static string GetFolder(Identifier speciesName)
|
||||
{
|
||||
@@ -314,25 +314,27 @@ namespace Barotrauma
|
||||
else if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
// Files found, but none specified -> Get a matching animation from the specified folder.
|
||||
// First try to find a file that matches the default file name. If that fails, just take any file.
|
||||
// First try to find a file that matches the default file name. If that fails, just take any file of the matching type.
|
||||
string defaultFileName = GetDefaultFileName(animSpecies, animType);
|
||||
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to get the specified file. If that fails, just take any file of the matching type.
|
||||
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
|
||||
if (selectedFile == null)
|
||||
{
|
||||
errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
|
||||
errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the first file of the matching type.");
|
||||
selectedFile = filteredFiles.First();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
|
||||
}
|
||||
selectedFile ??= GetDefaultFile(fallbackSpecies, animType);
|
||||
selectedFile ??= GetDefaultFilePath(fallbackSpecies, animType);
|
||||
Debug.Assert(selectedFile != null);
|
||||
if (errorMessages.None())
|
||||
{
|
||||
|
||||
@@ -147,7 +147,17 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Identifier or tag of the item the character's items are placed inside when the character despawns."), Editable]
|
||||
public Identifier DespawnContainer { get; private set; }
|
||||
|
||||
[Serialize("monster", IsPropertySaveable.Yes, description: "If changed, this character will try to play a custom music track with the specified identifier when encountered."), Editable]
|
||||
public Identifier MusicType { get; private set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The commonness of this character's music when a random track will be chosen."), Editable]
|
||||
public float MusicCommonness { get; private set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The multiplier of the minimum distance required between this character and the player/submarine before the music starts playing. The default distance is twice the length of the submarine, or a minimum of 50 meters."), Editable]
|
||||
public float MusicRangeMultiplier { get; private set; }
|
||||
|
||||
public readonly CharacterFile File;
|
||||
public bool IsPet => AI?.IsPet ?? false;
|
||||
|
||||
public XDocument VariantFile { get; private set; }
|
||||
|
||||
@@ -754,6 +764,8 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, "How likely it is that the creature plays dead (= ragdolls) while idling? Only allowed inside a sub (not in the open waters). Evaluated once, when the creature spawns."), Editable]
|
||||
public float PlayDeadProbability { get; set; }
|
||||
|
||||
public readonly bool IsPet;
|
||||
|
||||
public IEnumerable<TargetParams> Targets => targets;
|
||||
private readonly List<TargetParams> targets = new List<TargetParams>();
|
||||
@@ -763,6 +775,7 @@ namespace Barotrauma
|
||||
if (element == null) { return; }
|
||||
element.GetChildElements("target").ForEach(t => AddTarget(t));
|
||||
element.GetChildElements("targetpriority").ForEach(t => AddTarget(t));
|
||||
IsPet = element.GetChildElement("petbehavior") != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Barotrauma
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(200.0f, IsPropertySaveable.Yes)]
|
||||
[Serialize(200.0f, IsPropertySaveable.Yes, description: "The \"absolute\" maximum skill level with talents that increase the default maximum.")]
|
||||
public float MaximumSkillWithTalents
|
||||
{
|
||||
get;
|
||||
|
||||
+3
-4
@@ -69,21 +69,20 @@ namespace Barotrauma.Abilities
|
||||
switch (targetType)
|
||||
{
|
||||
case TargetType.Enemy:
|
||||
return !HumanAIController.IsFriendly(character, targetCharacter);
|
||||
return !HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: false);
|
||||
case TargetType.Ally:
|
||||
return HumanAIController.IsFriendly(character, targetCharacter);
|
||||
return HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: true);
|
||||
case TargetType.NotSelf:
|
||||
return targetCharacter != character;
|
||||
case TargetType.Alive:
|
||||
return !targetCharacter.IsDead;
|
||||
case TargetType.Monster:
|
||||
return !targetCharacter.IsHuman;
|
||||
return !targetCharacter.IsHuman && !targetCharacter.IsPet;
|
||||
case TargetType.InFriendlySubmarine:
|
||||
return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ namespace Barotrauma.Abilities
|
||||
}
|
||||
if (!nearbyCharactersAppliesToAllies)
|
||||
{
|
||||
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
|
||||
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character, onlySameTeam: true));
|
||||
}
|
||||
if (!nearbyCharactersAppliesToEnemies)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Items.Components;
|
||||
@@ -182,7 +182,10 @@ namespace Barotrauma
|
||||
To.Connection.DisconnectWire(wire);
|
||||
}
|
||||
// if EntitySpawner is not available
|
||||
wireItem.Remove();
|
||||
if (!wireItem.Removed)
|
||||
{
|
||||
wireItem.Remove();
|
||||
}
|
||||
}
|
||||
|
||||
public static ItemPrefab DefaultWirePrefab => ItemPrefab.Prefabs[Tags.RedWire];
|
||||
|
||||
+22
-3
@@ -1,9 +1,28 @@
|
||||
namespace Barotrauma
|
||||
namespace Barotrauma
|
||||
{
|
||||
sealed class BackgroundCreaturePrefabsFile : OtherFile
|
||||
#if CLIENT
|
||||
[NotSyncedInMultiplayer]
|
||||
sealed class BackgroundCreaturePrefabsFile : GenericPrefabFile<BackgroundCreaturePrefab>
|
||||
{
|
||||
public BackgroundCreaturePrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
|
||||
|
||||
//this content type only comes into play when a level is generated, so LoadFile and UnloadFile don't have anything to do
|
||||
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
|
||||
protected override bool MatchesPlural(Identifier identifier) => identifier == "backgroundcreatures";
|
||||
protected override PrefabCollection<BackgroundCreaturePrefab> Prefabs => BackgroundCreaturePrefab.Prefabs;
|
||||
protected override BackgroundCreaturePrefab CreatePrefab(ContentXElement element)
|
||||
{
|
||||
return new BackgroundCreaturePrefab(element, this);
|
||||
}
|
||||
|
||||
public sealed override Md5Hash CalculateHash() => Md5Hash.Blank;
|
||||
}
|
||||
#else
|
||||
[NotSyncedInMultiplayer]
|
||||
sealed class BackgroundCreaturePrefabsFile : OtherFile
|
||||
{
|
||||
public BackgroundCreaturePrefabsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path)
|
||||
{
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+111
-2
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml;
|
||||
using Barotrauma.IO;
|
||||
using Steamworks.Data;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -44,6 +45,27 @@ namespace Barotrauma
|
||||
|
||||
public readonly Version GameVersion;
|
||||
public readonly string ModVersion;
|
||||
|
||||
public enum UgcStatus
|
||||
{
|
||||
NotFetched = 0,
|
||||
Fetching = 1,
|
||||
Fetched = 2,
|
||||
Unavailable = 3
|
||||
}
|
||||
|
||||
public UgcStatus UgcItemStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ugc item (workshop item) data that this content package corresponds to. Needs to be fetched with <see cref="TryFetchUgcItem(Action)"/>.
|
||||
/// You can also check <see cref="UgcItemStatus"/> to see if the item is available or not.
|
||||
/// </summary>
|
||||
public Option<Steamworks.Ugc.Item> UgcItem
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Md5Hash Hash { get; private set; }
|
||||
public readonly Option<SerializableDateTime> InstallTime;
|
||||
|
||||
@@ -65,8 +87,15 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public Option<ContentPackageManager.LoadProgress.Error> EnableError { get; private set; }
|
||||
= Option.None;
|
||||
|
||||
public bool HasAnyErrors => FatalLoadErrors.Length > 0 || EnableError.IsSome();
|
||||
|
||||
private readonly HashSet<PublishedFileId> missingDependencies = new HashSet<PublishedFileId>();
|
||||
/// <summary>
|
||||
/// An error caused by missing dependencies (Workshop items required by the package).
|
||||
/// Can be safe to ignore.
|
||||
/// </summary>
|
||||
public IEnumerable<PublishedFileId> MissingDependencies => missingDependencies;
|
||||
|
||||
public bool HasAnyErrors => FatalLoadErrors.Length > 0 || EnableError.IsSome() || missingDependencies.Any();
|
||||
|
||||
public async Task<bool> IsUpToDate()
|
||||
{
|
||||
@@ -232,6 +261,16 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMissingDependency(PublishedFileId missingItemID)
|
||||
{
|
||||
missingDependencies.Add(missingItemID);
|
||||
}
|
||||
|
||||
public void ClearMissingDependencies()
|
||||
{
|
||||
missingDependencies.Clear();
|
||||
}
|
||||
|
||||
public void LoadFilesOfType<T>() where T : ContentFile
|
||||
{
|
||||
Files.Where(f => f is T).ForEach(f => f.LoadFile());
|
||||
@@ -328,6 +367,76 @@ namespace Barotrauma
|
||||
errorCatcher.Dispose();
|
||||
}
|
||||
|
||||
public void TryFetchUgcDescription(Action<string?> onFinished)
|
||||
{
|
||||
TryFetchUgcItem((Steamworks.Ugc.Item? item) =>
|
||||
{
|
||||
onFinished?.Invoke(item?.Description ?? string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
public void TryFetchUgcChildren(Action<PublishedFileId[]?> onFinished)
|
||||
{
|
||||
TryFetchUgcItem((Steamworks.Ugc.Item? item) =>
|
||||
{
|
||||
onFinished?.Invoke(item?.Children ?? Array.Empty<PublishedFileId>());
|
||||
});
|
||||
}
|
||||
|
||||
private void TryFetchUgcItem(Action<Steamworks.Ugc.Item?> onFinished)
|
||||
{
|
||||
switch (UgcItemStatus)
|
||||
{
|
||||
case UgcStatus.NotFetched:
|
||||
TryFetchUgcItem(onFinished: () =>
|
||||
{
|
||||
if (UgcItemStatus == UgcStatus.Fetched &&
|
||||
UgcItem.TryUnwrap(out var cachedItem))
|
||||
{
|
||||
onFinished?.Invoke(cachedItem);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case UgcStatus.Fetched when UgcItem.TryUnwrap(out var cachedItem):
|
||||
onFinished?.Invoke(cachedItem);
|
||||
break;
|
||||
default:
|
||||
onFinished?.Invoke(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to fetch the UgcItem (workshop item) data from Steamworks, and if successful, caches it in <see cref="UgcItem"/>.
|
||||
/// </summary>
|
||||
/// <param name="onFinished">Triggers when the query finishes or fails (or immediately if the item has been already cached)</param>
|
||||
public void TryFetchUgcItem(Action onFinished)
|
||||
{
|
||||
if (UgcItemStatus != UgcStatus.NotFetched)
|
||||
{
|
||||
onFinished?.Invoke();
|
||||
}
|
||||
if (!UgcId.TryUnwrap(out var ugcId) || ugcId is not SteamWorkshopId workshopId)
|
||||
{
|
||||
UgcItemStatus = UgcStatus.Unavailable;
|
||||
return;
|
||||
}
|
||||
|
||||
UgcItemStatus = UgcStatus.Fetching;
|
||||
TaskPool.Add($"PrepareToShow{UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value),
|
||||
task =>
|
||||
{
|
||||
if (!task.TryGetResult(out Option<Steamworks.Ugc.Item> itemOption) || !itemOption.TryUnwrap(out var item))
|
||||
{
|
||||
UgcItemStatus = UgcStatus.Unavailable;
|
||||
return;
|
||||
}
|
||||
UgcItem = Option<Steamworks.Ugc.Item>.Some(item);
|
||||
UgcItemStatus = UgcStatus.Fetched;
|
||||
onFinished?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
public void UnloadContent()
|
||||
{
|
||||
Files.ForEach(f => f.UnloadFile());
|
||||
|
||||
+21
-1
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Collections;
|
||||
@@ -569,6 +569,26 @@ namespace Barotrauma
|
||||
yield return LoadProgress.Progress(1.0f);
|
||||
}
|
||||
|
||||
public static void CheckMissingDependencies()
|
||||
{
|
||||
foreach (var enabledPackage in EnabledPackages.All)
|
||||
{
|
||||
enabledPackage.ClearMissingDependencies();
|
||||
enabledPackage.TryFetchUgcChildren((Steamworks.Data.PublishedFileId[]? children) =>
|
||||
{
|
||||
if (children == null) { return; }
|
||||
var missingChildren = children
|
||||
.Where(childUgcItemId =>
|
||||
EnabledPackages.All.None(package =>
|
||||
package.UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId workshopId && workshopId.Value == childUgcItemId.Value));
|
||||
foreach (var missingChild in missingChildren)
|
||||
{
|
||||
enabledPackage.AddMissingDependency(missingChild);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogEnabledRegularPackageErrors()
|
||||
{
|
||||
foreach (var p in EnabledPackages.Regular)
|
||||
|
||||
@@ -296,6 +296,19 @@ namespace Barotrauma
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("spawnnpc", "spawnnpc [any/npcsetidentifier] [npcidentifier] [near/inside/outside/cursor] [team (0-3)] [add to crew (true/false)]: Spawns an pre-configured NPC at a random spawnpoint. (Use the third parameter to select a specific set of spawnpoints.)", onExecute: null,
|
||||
getValidArgs: () =>
|
||||
{
|
||||
return new string[][]
|
||||
{
|
||||
"any".ToEnumerable().Union(NPCSet.Sets.Select(p => p.Identifier.Value).OrderBy(s => s)).ToArray(), // NPC Sets
|
||||
NPCSet.Sets.SelectMany(set => set.Humans).Select(p => p.Identifier.Value).OrderBy(s => s).ToArray(), // NPCs
|
||||
new string[] { "near", "inside", "outside", "cursor" },
|
||||
Enum.GetValues<CharacterTeamType>().Select(v => v.ToString()).ToArray(),
|
||||
new string[] { "true", "false" }
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
|
||||
(string[] args) =>
|
||||
{
|
||||
@@ -570,12 +583,10 @@ namespace Barotrauma
|
||||
onExecute: null,
|
||||
getValidArgs:() =>
|
||||
{
|
||||
var characterList = Character.Controlled != null ? new[] { "Me" } : Array.Empty<string>();
|
||||
var subList = Submarine.MainSub != null ? new[] { "mainsub" } : Array.Empty<string>();
|
||||
return new string[][]
|
||||
{
|
||||
characterList.Concat(ListCharacterNames()).ToArray(),
|
||||
subList.Concat(ListAvailableLocations()).ToArray()
|
||||
ListCharacterNames(includeMeArgument: Character.Controlled != null, includeCrewArgument: true),
|
||||
ListAvailableLocations()
|
||||
};
|
||||
}, isCheat: true));
|
||||
|
||||
@@ -647,16 +658,19 @@ namespace Barotrauma
|
||||
commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
|
||||
(string[] args) =>
|
||||
{
|
||||
Character targetCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false);
|
||||
|
||||
if (targetCharacter == null) { return; }
|
||||
|
||||
targetCharacter.GodMode = !targetCharacter.GodMode;
|
||||
NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on ") + targetCharacter.Name, Color.White);
|
||||
bool? godmodeStateOnFirstCharacter = null;
|
||||
HandleCommandForCrewOrSingleCharacter(args, ToggleGodMode);
|
||||
void ToggleGodMode(Character targetCharacter)
|
||||
{
|
||||
targetCharacter.GodMode = godmodeStateOnFirstCharacter ?? !targetCharacter.GodMode;
|
||||
godmodeStateOnFirstCharacter = targetCharacter.GodMode;
|
||||
NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on ") + targetCharacter.Name,
|
||||
targetCharacter.GodMode ? Color.LimeGreen : Color.Gray);
|
||||
}
|
||||
},
|
||||
() =>
|
||||
{
|
||||
return new string[][] { ListCharacterNames() };
|
||||
return new string[][] { ListCharacterNames(includeMeArgument: Character.Controlled != null, includeCrewArgument: true) };
|
||||
}, isCheat: true));
|
||||
|
||||
commands.Add(new Command("godmode_mainsub", "godmode_mainsub: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) =>
|
||||
@@ -813,17 +827,13 @@ namespace Barotrauma
|
||||
commands.Add(new Command("heal", "heal [character name] [all]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed. By default only heals common afflictions such as physical damage and blood loss: use the \"all\" argument to heal everything, including poisonings/addictions/etc.", (string[] args) =>
|
||||
{
|
||||
bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase);
|
||||
Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args);
|
||||
if (healedCharacter != null)
|
||||
{
|
||||
HealCharacter(healedCharacter, healAll);
|
||||
}
|
||||
HandleCommandForCrewOrSingleCharacter(args, (Character targetCharacter) => HealCharacter(targetCharacter, healAll));
|
||||
},
|
||||
() =>
|
||||
{
|
||||
return new string[][]
|
||||
{
|
||||
Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(),
|
||||
ListCharacterNames(includeMeArgument: true, includeCrewArgument: true),
|
||||
new string[] { "all" }
|
||||
};
|
||||
}, isCheat: true));
|
||||
@@ -1858,6 +1868,15 @@ namespace Barotrauma
|
||||
}
|
||||
}, null, isCheat: true));
|
||||
|
||||
commands.Add(new Command("killall", "killall: Immediately kills all characters in the level.", args =>
|
||||
{
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
c.Kill(CauseOfDeathType.Unknown, null);
|
||||
NewMessage($"Killed {c.DisplayName}.");
|
||||
}
|
||||
}, null, isCheat: true));
|
||||
|
||||
commands.Add(new Command("despawnnow", "despawnnow [character]: Immediately despawns the specified dead character. If the character argument is omitted, all dead characters are despawned.", (string[] args) =>
|
||||
{
|
||||
if (args.Length == 0)
|
||||
@@ -2285,7 +2304,7 @@ namespace Barotrauma
|
||||
commands.Sort((c1, c2) => c1.Names.First().CompareTo(c2.Names.First()));
|
||||
}
|
||||
|
||||
private static void HealCharacter(Character healedCharacter, bool healAll)
|
||||
private static void HealCharacter(Character healedCharacter, bool healAll, Client targetClient = null)
|
||||
{
|
||||
healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f);
|
||||
healedCharacter.Oxygen = 100.0f;
|
||||
@@ -2299,6 +2318,12 @@ namespace Barotrauma
|
||||
string characterNameText = healedCharacter == Character.Controlled ? $"{healedCharacter.Name} (you)" : healedCharacter.Name;
|
||||
string text = healAll ? $"Healed {characterNameText}: all afflictions" : $"Healed {characterNameText}: damage and common afflictions";
|
||||
NewMessage(text, Color.Yellow);
|
||||
#if SERVER
|
||||
if (targetClient != null)
|
||||
{
|
||||
GameMain.Server.SendConsoleMessage(text, targetClient);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public static string AutoComplete(string command, int increment = 1)
|
||||
@@ -2479,7 +2504,7 @@ namespace Barotrauma
|
||||
private static string[] ListAvailableLocations()
|
||||
{
|
||||
List<string> locationNames = new();
|
||||
foreach(var submarine in Submarine.Loaded)
|
||||
foreach (var submarine in Submarine.Loaded)
|
||||
{
|
||||
locationNames.Add(submarine.Info.Name);
|
||||
}
|
||||
@@ -2498,7 +2523,10 @@ namespace Barotrauma
|
||||
locationNames.Add($"{caveName}_{index}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (Submarine.MainSub != null) { locationNames.Add("mainsub"); }
|
||||
locationNames.Add("cursor");
|
||||
|
||||
return locationNames.ToArray();
|
||||
}
|
||||
|
||||
@@ -2663,9 +2691,17 @@ namespace Barotrauma
|
||||
|
||||
private static IOrderedEnumerable<Character> SortSpawnedSpecies(IEnumerable<Character> characterList) => characterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name);
|
||||
|
||||
private static string[] ListCharacterNames() => GetCharacterNames();
|
||||
private static string[] ListCharacterNames(bool includeMeArgument = false, bool includeCrewArgument = false) =>
|
||||
GetCharacterNames(includeMeArgument, includeCrewArgument);
|
||||
|
||||
private static string[] GetCharacterNames() => SortSpawnedSpecies(Character.CharacterList).Select(c => c.Name).Distinct().ToArray();
|
||||
private static string[] GetCharacterNames(bool includeMeArgument = false, bool includeCrewArgument = false)
|
||||
{
|
||||
var characterNames = new List<string>();
|
||||
if (includeMeArgument) { characterNames.Add("/me"); }
|
||||
if (includeCrewArgument) { characterNames.Add("/crew"); }
|
||||
characterNames.AddRange(SortSpawnedSpecies(Character.CharacterList).Select(c => c.Name));
|
||||
return characterNames.ToArray();
|
||||
}
|
||||
|
||||
private static string[] GetSpawnedSpeciesNames() => SortSpawnedSpecies(Character.CharacterList).Select(c => c.SpeciesName.Value).Distinct().ToArray();
|
||||
|
||||
@@ -2678,6 +2714,28 @@ namespace Barotrauma
|
||||
|
||||
private static IEnumerable<Character> FindMatchingSpecies(string speciesName) => Character.CharacterList.FindAll(c => c.SpeciesName.Value.Equals(speciesName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the arguments specify a specific character, or if they target the crew, and executes the specified action on them.
|
||||
/// </summary>
|
||||
private static void HandleCommandForCrewOrSingleCharacter(string[] args, Action<Character> action, Client targetClient = null)
|
||||
{
|
||||
if (args.Length > 0 && args.First() == "/crew")
|
||||
{
|
||||
foreach (var crewCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
action(crewCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Character targetCharacter = (args.Length == 0 || args.First() == "/me") ?
|
||||
targetClient?.Character ?? Character.Controlled :
|
||||
FindMatchingCharacter(args, false);
|
||||
if (targetCharacter == null) { return; }
|
||||
action(targetCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null, bool botsOnly = false)
|
||||
{
|
||||
if (args.Length == 0) { return null; }
|
||||
@@ -2687,7 +2745,11 @@ namespace Barotrauma
|
||||
int characterIndex = -1;
|
||||
foreach (string arg in args)
|
||||
{
|
||||
// First try to parse the character name from all the arguments.
|
||||
if (arg == "/me")
|
||||
{
|
||||
return allowedRemotePlayer?.Character ?? Character.Controlled;
|
||||
}
|
||||
// try to parse the character name from all the arguments.
|
||||
if (matchingCharacters == null || matchingCharacters.None())
|
||||
{
|
||||
string possibleCharacterName = arg?.ToLowerInvariant();
|
||||
@@ -2753,15 +2815,14 @@ namespace Barotrauma
|
||||
Character targetCharacter = controlledCharacter;
|
||||
Vector2 worldPosition = cursorWorldPos;
|
||||
string locationNameArgument = "";
|
||||
string firstArgument = args.FirstOrDefault()?.ToLowerInvariant() ?? string.Empty;
|
||||
if (args.Length > 0)
|
||||
{
|
||||
string firstArgument = args.First().ToLowerInvariant();
|
||||
string lastArgument = args.Last();
|
||||
// First seek the matching character.
|
||||
if (firstArgument != "me")
|
||||
if (firstArgument is not ("/me" or "/crew"))
|
||||
{
|
||||
var availableLocations = Submarine.MainSub != null ? new[] { "mainsub", "cursor" } : Array.Empty<string>();
|
||||
availableLocations = availableLocations.Concat(ListAvailableLocations()).ToArray();
|
||||
var availableLocations = ListAvailableLocations();
|
||||
if (args.Length > 1 || availableLocations.None(locationName => string.Equals(locationName, lastArgument, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// Try to find a matching character, if there's more than one argument or if the last argument is not a valid location argument.
|
||||
@@ -2770,12 +2831,31 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
// Then seek the possible location argument.
|
||||
if (targetCharacter == null || !targetCharacter.Name.Equals(lastArgument, StringComparison.OrdinalIgnoreCase) && !int.TryParse(lastArgument, out _))
|
||||
if (args.Count() > 1)
|
||||
{
|
||||
locationNameArgument = lastArgument;
|
||||
if (targetCharacter == null || !targetCharacter.Name.Equals(lastArgument, StringComparison.OrdinalIgnoreCase) &&
|
||||
!int.TryParse(lastArgument, out _))
|
||||
{
|
||||
locationNameArgument = lastArgument;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstArgument == "/crew")
|
||||
{
|
||||
foreach (var crewCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
TeleportSpecificCharacter(crewCharacter, locationNameArgument, worldPosition);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TeleportSpecificCharacter(targetCharacter, locationNameArgument, worldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TeleportSpecificCharacter(Character targetCharacter, string locationNameArgument, Vector2 defaultWorldPosition)
|
||||
{
|
||||
Vector2 worldPosition = defaultWorldPosition;
|
||||
if (!string.IsNullOrWhiteSpace(locationNameArgument) && !string.Equals(locationNameArgument, "cursor", comparisonType: StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition))
|
||||
@@ -2788,7 +2868,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
targetCharacter.TeleportTo(worldPosition);
|
||||
@@ -2800,133 +2880,172 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg)
|
||||
/// <param name="usePreConfiguredNPC">Should we spawn a preconfigured NPC from an <see cref="NPCSet"/>? If so, the first 2 arguments are expected to be the identifier of the NPC set and the identifier of the NPC.</param>
|
||||
private static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, bool usePreConfiguredNPC = false)
|
||||
{
|
||||
errorMsg = "";
|
||||
if (args.Length == 0) { return; }
|
||||
int characterArgumentCount = 1;
|
||||
if (usePreConfiguredNPC)
|
||||
{
|
||||
//two arguments required for NPCs, identifier of the NPC set and identifier of the NPC.
|
||||
characterArgumentCount = 2;
|
||||
}
|
||||
|
||||
Character spawnedCharacter = null;
|
||||
if (args.Length < characterArgumentCount) { return; }
|
||||
for (int i = 0; i < characterArgumentCount; i++)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(args[i])) { return; }
|
||||
}
|
||||
|
||||
Vector2 spawnPosition = Vector2.Zero;
|
||||
WayPoint spawnPoint = null;
|
||||
|
||||
string characterLowerCase = args[0].ToLowerInvariant();
|
||||
JobPrefab job = null;
|
||||
if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase))
|
||||
bool isHuman = true;
|
||||
if (!usePreConfiguredNPC)
|
||||
{
|
||||
job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
else
|
||||
{
|
||||
job = JobPrefab.Prefabs[characterLowerCase];
|
||||
}
|
||||
bool isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
switch (args[1].ToLowerInvariant())
|
||||
string characterLowerCase = args[0].ToLowerInvariant();
|
||||
if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase))
|
||||
{
|
||||
case "inside":
|
||||
spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub) ?? WayPoint.GetRandom(SpawnType.Human, assignedJob: null, Submarine.MainSub);
|
||||
break;
|
||||
case "outside":
|
||||
spawnPoint = WayPoint.GetRandom(SpawnType.Enemy);
|
||||
break;
|
||||
case "near":
|
||||
case "close":
|
||||
float closestDist = -1.0f;
|
||||
foreach (WayPoint wp in WayPoint.WayPointList)
|
||||
{
|
||||
if (wp.Submarine != null) continue;
|
||||
|
||||
//don't spawn inside hulls
|
||||
if (Hull.FindHull(wp.WorldPosition, null) != null) continue;
|
||||
|
||||
float dist = Vector2.Distance(wp.WorldPosition, GameMain.GameScreen.Cam.WorldViewCenter);
|
||||
|
||||
if (closestDist < 0.0f || dist < closestDist)
|
||||
{
|
||||
spawnPoint = wp;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "cursor":
|
||||
spawnPosition = cursorWorldPos;
|
||||
break;
|
||||
default:
|
||||
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
|
||||
break;
|
||||
job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(args[0])) { return; }
|
||||
|
||||
CharacterTeamType defaultTeamType = isHuman ? CharacterTeamType.Team1 : CharacterTeamType.None;
|
||||
CharacterTeamType teamType = defaultTeamType;
|
||||
if (args.Length > 2)
|
||||
{
|
||||
string team = args[2];
|
||||
if (!Enum.TryParse(team, ignoreCase: true, out teamType) && !string.IsNullOrWhiteSpace(team))
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
teamType = (CharacterTeamType)int.Parse(team);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ThrowError($"\"{team}\" is not a valid team id.");
|
||||
}
|
||||
job = JobPrefab.Prefabs[characterLowerCase];
|
||||
}
|
||||
isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
|
||||
}
|
||||
|
||||
bool addToCrew = isHuman && teamType == defaultTeamType;
|
||||
if (args.Length > 3)
|
||||
{
|
||||
addToCrew = args[3].Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
|
||||
|
||||
if (isHuman)
|
||||
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType teamType, out bool addToCrew);
|
||||
|
||||
if (usePreConfiguredNPC)
|
||||
{
|
||||
var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0;
|
||||
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
|
||||
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
|
||||
}
|
||||
else
|
||||
{
|
||||
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier());
|
||||
if (prefab != null)
|
||||
Identifier npcSetIdentifier = args[0].ToIdentifier();
|
||||
Identifier humanPrefabIdentifier = args[1].ToIdentifier();
|
||||
HumanPrefab humanPrefab =
|
||||
npcSetIdentifier == "any" ?
|
||||
NPCSet.Sets.SelectMany(set => set.Humans).FirstOrDefault(human => human.Identifier == humanPrefabIdentifier) :
|
||||
NPCSet.Get(npcSetIdentifier, humanPrefabIdentifier);
|
||||
if (humanPrefab != null)
|
||||
{
|
||||
CharacterInfo characterInfo = null;
|
||||
if (prefab.HasCharacterInfo)
|
||||
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, humanPrefab.CreateCharacterInfo(), onSpawn: newCharacter =>
|
||||
{
|
||||
characterInfo = new CharacterInfo(prefab.Identifier);
|
||||
}
|
||||
spawnedCharacter = Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo);
|
||||
}
|
||||
}
|
||||
if (spawnedCharacter == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to spawn a character with the provided arguments!");
|
||||
return;
|
||||
}
|
||||
spawnedCharacter.TeamID = teamType;
|
||||
#if CLIENT
|
||||
if (addToCrew && GameMain.GameSession?.CrewManager is CrewManager crewManager)
|
||||
{
|
||||
crewManager.AddCharacter(spawnedCharacter);
|
||||
}
|
||||
newCharacter.HumanPrefab = humanPrefab;
|
||||
AddToCrew(newCharacter);
|
||||
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine, spawnPoint);
|
||||
humanPrefab.InitializeCharacter(newCharacter);
|
||||
#if SERVER
|
||||
newCharacter.LoadTalents();
|
||||
GameMain.NetworkMember.CreateEntityEvent(newCharacter, new Character.UpdateTalentsEventData());
|
||||
#endif
|
||||
if (isHuman)
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (isHuman)
|
||||
{
|
||||
spawnedCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint);
|
||||
spawnedCharacter.GiveIdCardTags(spawnPoint);
|
||||
spawnedCharacter.Info.StartItemsGiven = true;
|
||||
int variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0;
|
||||
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
|
||||
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter =>
|
||||
{
|
||||
AddToCrew(newCharacter);
|
||||
newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint);
|
||||
newCharacter.GiveIdCardTags(spawnPoint);
|
||||
newCharacter.Info.StartItemsGiven = true;
|
||||
});
|
||||
}
|
||||
else if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) is { } prefab)
|
||||
{
|
||||
Entity.Spawner.AddCharacterToSpawnQueue(args[0].ToIdentifier(), spawnPosition, prefab.HasCharacterInfo ? new CharacterInfo(prefab.Identifier) : null);
|
||||
}
|
||||
|
||||
void AddToCrew(Character newCharacter)
|
||||
{
|
||||
newCharacter.TeamID = teamType;
|
||||
if (addToCrew)
|
||||
{
|
||||
GameMain.GameSession?.CrewManager.AddCharacter(newCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType teamType, out bool addToCrew)
|
||||
{
|
||||
spawnPosition = Vector2.Zero;
|
||||
spawnPoint = null;
|
||||
|
||||
int argIndex = characterArgumentCount;
|
||||
if (args.Length > argIndex)
|
||||
{
|
||||
switch (args[argIndex].ToLowerInvariant())
|
||||
{
|
||||
case "inside":
|
||||
spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub);
|
||||
break;
|
||||
case "outside":
|
||||
spawnPoint = WayPoint.GetRandom(SpawnType.Enemy);
|
||||
break;
|
||||
case "near":
|
||||
case "close":
|
||||
float closestDist = -1f;
|
||||
foreach (WayPoint wp in WayPoint.WayPointList)
|
||||
{
|
||||
if (wp.Submarine != null) { continue; }
|
||||
|
||||
// Don't spawn inside hulls
|
||||
if (Hull.FindHull(wp.WorldPosition, null) != null) { continue; }
|
||||
|
||||
float dist = Vector2.Distance(wp.WorldPosition, GameMain.GameScreen.Cam.WorldViewCenter);
|
||||
|
||||
if (closestDist < 0f || dist < closestDist)
|
||||
{
|
||||
spawnPoint = wp;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "cursor":
|
||||
spawnPosition = cursorWorldPos;
|
||||
break;
|
||||
default:
|
||||
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
|
||||
}
|
||||
if (spawnPoint != null)
|
||||
{
|
||||
spawnPosition = spawnPoint.WorldPosition;
|
||||
}
|
||||
|
||||
argIndex++;
|
||||
if (args.Length > argIndex)
|
||||
{
|
||||
if (int.TryParse(args[argIndex], out int teamID) && teamID is >= 0 and <= 3)
|
||||
{
|
||||
teamType = (CharacterTeamType)teamID;
|
||||
}
|
||||
else if (!Enum.TryParse(args[argIndex], ignoreCase: true, out teamType))
|
||||
{
|
||||
teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
|
||||
ThrowError($"\"{args[argIndex]}\" is not a valid team id. Defaulting to {teamType}.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
|
||||
}
|
||||
|
||||
argIndex++;
|
||||
addToCrew = isHuman;
|
||||
if (args.Length > argIndex)
|
||||
{
|
||||
if (bool.TryParse(args[argIndex], out bool result))
|
||||
{
|
||||
addToCrew = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3350,7 +3469,7 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
private static IEnumerable<CoroutineStatus> CreateMessageBox(string errorMsg)
|
||||
{
|
||||
new GUIMessageBox(TextManager.Get("Error"), errorMsg);
|
||||
new GUIMessageBox(TextManager.Get("Error"), errorMsg, minSize: new Point(GUI.IntScale(700), GUI.IntScale(500)));
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Barotrauma
|
||||
SortCategory = element.GetAttributeIdentifier("sortcategory", Identifier);
|
||||
Prerequisite = element.GetAttributeIdentifier("prerequisite", Identifier.Empty);
|
||||
MutuallyExclusivePerks = element.GetAttributeIdentifierImmutableHashSet("mutuallyexclusiveperks", ImmutableHashSet<Identifier>.Empty);
|
||||
SortKey = element.GetAttributeInt("sortkey", ToolBox.IdentifierToInt(Identifier));
|
||||
SortKey = element.GetAttributeInt("sortkey", 0);
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<PerkBase>();
|
||||
foreach (var child in element.Elements())
|
||||
|
||||
@@ -760,4 +760,12 @@ namespace Barotrauma
|
||||
PlayerPreference,
|
||||
PlayerChoice,
|
||||
}
|
||||
|
||||
public enum BotStatus
|
||||
{
|
||||
PendingHireToActiveService,
|
||||
PendingHireToReserveBench,
|
||||
ActiveService,
|
||||
ReserveBench
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -25,6 +25,9 @@ namespace Barotrauma
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "A tag to apply to the hull the target is currently in when the check succeeds.")]
|
||||
public Identifier ApplyTagToHull { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Tag to apply to the target (or all targets if there's multiple) when the check succeeds.")]
|
||||
public Identifier ApplyTagToTarget { get; set; }
|
||||
|
||||
public CheckConditionalAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
{
|
||||
@@ -84,7 +87,7 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (var target in targets)
|
||||
{
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
ApplyTagsToTarget(target);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -95,12 +98,21 @@ namespace Barotrauma
|
||||
{
|
||||
if (ConditionalsMatch(target))
|
||||
{
|
||||
ApplyTagsToTarget(target);
|
||||
success = true;
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
void ApplyTagsToTarget(ISerializableEntity target)
|
||||
{
|
||||
if (!ApplyTagToTarget.IsEmpty)
|
||||
{
|
||||
ParentEvent.AddTarget(ApplyTagToTarget, target as Entity);
|
||||
}
|
||||
ApplyTagsToHulls(target as Entity, ApplyTagToHull, ApplyTagToLinkedHulls);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ConditionalsMatch(ISerializableEntity target)
|
||||
|
||||
@@ -84,6 +84,8 @@ namespace Barotrauma
|
||||
//an identifier the server uses to identify which ConversationAction a client is responding to
|
||||
public readonly UInt16 Identifier;
|
||||
|
||||
private float startDelay;
|
||||
|
||||
private int selectedOption = -1;
|
||||
private bool dialogOpened = false;
|
||||
|
||||
@@ -167,7 +169,11 @@ namespace Barotrauma
|
||||
#else
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (c.InGame && c.Character != null) { ServerWrite(Speaker, c, interrupt); }
|
||||
if (c.InGame && c.Character != null)
|
||||
{
|
||||
DebugConsole.Log($"Conversation {ParentEvent.Prefab.Identifier} finished, communicating to clients...");
|
||||
ServerWrite(Speaker, c, interrupt);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
ResetSpeaker();
|
||||
@@ -211,6 +217,16 @@ namespace Barotrauma
|
||||
Speaker = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retriggers the conversation after the specified delay.
|
||||
/// </summary>
|
||||
public void RetriggerAfter(float delay)
|
||||
{
|
||||
startDelay = delay;
|
||||
dialogOpened = false;
|
||||
selectedOption = -1;
|
||||
}
|
||||
|
||||
public override bool SetGoToTarget(string goTo)
|
||||
{
|
||||
selectedOption = -1;
|
||||
@@ -264,6 +280,9 @@ namespace Barotrauma
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
startDelay -= deltaTime;
|
||||
if (startDelay > 0) { return; }
|
||||
|
||||
if (interrupt)
|
||||
{
|
||||
Interrupted?.Update(deltaTime);
|
||||
@@ -400,9 +419,22 @@ namespace Barotrauma
|
||||
{
|
||||
targets = ParentEvent.GetTargets(TargetTag).Where(e => IsValidTarget(e));
|
||||
if (!targets.Any() || IsBlockedByAnotherConversation(targets, BlockOtherConversationsDuration)) { return; }
|
||||
//some specific character tried to start the convo, but not included in the targets for this conversation -> disallow
|
||||
if (targetCharacter != null && !targets.Contains(targetCharacter)) { return; }
|
||||
}
|
||||
else
|
||||
{
|
||||
#if SERVER
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
//conversation targeted to everyone, but no-one present yet who could potentially hear it -> don't start yet
|
||||
UpdateIgnoredClients();
|
||||
if (GameMain.NetworkMember.ConnectedClients.None(c => CanClientReceive(c))) { return; }
|
||||
}
|
||||
#endif
|
||||
if (IsBlockedByAnotherConversation(targetCharacter?.ToEnumerable(), BlockOtherConversationsDuration)) { return; }
|
||||
}
|
||||
|
||||
if (IsBlockedByAnotherConversation(targetCharacter?.ToEnumerable(), BlockOtherConversationsDuration)) { return; }
|
||||
|
||||
if (speaker?.AIController is HumanAIController humanAI)
|
||||
{
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Barotrauma
|
||||
ChangeItemTeam(Submarine.MainSub ?? Submarine.Loaded.FirstOrDefault(s => s.TeamID == TeamID), allowStealing: true);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.AllItems));
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AddToCrewEventData(TeamID, npc.Inventory.FindAllItems(recursive: true)));
|
||||
}
|
||||
}
|
||||
else if (RemoveFromCrew && npc.TeamID is CharacterTeamType.Team1 or CharacterTeamType.Team2)
|
||||
@@ -95,7 +95,7 @@ namespace Barotrauma
|
||||
ChangeItemTeam(sub, false);
|
||||
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.AllItems));
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.RemoveFromCrewEventData(TeamID, npc.Inventory.FindAllItems(recursive: true)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -22,6 +23,20 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop following the target when the event resets?")]
|
||||
public bool AbandonOnReset { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
|
||||
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
|
||||
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
|
||||
"A priority of 60 would make the objective work like a lowest priority order." +
|
||||
"So, if we'll want the character to follow, but still be able to find safety, defend themselves when attacked, or flee from dangers," +
|
||||
"it's better to use e.g. 70 instead of 100.")]
|
||||
public float Priority
|
||||
{
|
||||
get => _priority;
|
||||
set => _priority = Math.Clamp(value, AIObjectiveManager.LowestOrderPriority, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
|
||||
private float _priority;
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
@@ -39,7 +54,7 @@ namespace Barotrauma
|
||||
if (target == null) { return; }
|
||||
|
||||
int targetCount = 0;
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character);
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).OfType<Character>();
|
||||
foreach (var npc in affectedNpcs)
|
||||
{
|
||||
if (npc.Removed) { continue; }
|
||||
@@ -49,7 +64,7 @@ namespace Barotrauma
|
||||
{
|
||||
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
|
||||
{
|
||||
OverridePriority = 100.0f,
|
||||
OverridePriority = Priority,
|
||||
IsFollowOrder = true
|
||||
};
|
||||
humanAiController.ObjectiveManager.AddObjective(newObjective);
|
||||
|
||||
+14
-2
@@ -2,6 +2,7 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -31,8 +32,19 @@ namespace Barotrauma
|
||||
[Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs the action can target. For example, you could only make a specific number of security officers man a periscope.")]
|
||||
public int MaxTargets { get; set; }
|
||||
|
||||
[Serialize(100, IsPropertySaveable.Yes, description: "Priority of operating the item (0-100). Higher values will make the AI prefer operating the item over other orders (priority 60-70) or e.g. reacting to emergencies (priority 90).")]
|
||||
public int Priority { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
|
||||
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
|
||||
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
|
||||
"A priority of 60 would make the objective work like a lowest priority order." +
|
||||
"So, if we'll want the character to operate the item, but still be able to find safety, defend themselves when attacked, or flee from dangers," +
|
||||
"it's better to use e.g. 70 instead of 100.")]
|
||||
public float Priority
|
||||
{
|
||||
get => _priority;
|
||||
set => _priority = Math.Clamp(value, AIObjectiveManager.LowestOrderPriority, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
private float _priority;
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop operating the item when the event resets?")]
|
||||
public bool AbandonOnReset { get; set; }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -13,6 +14,20 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop waiting?")]
|
||||
public bool Wait { get; set; }
|
||||
|
||||
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
|
||||
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
|
||||
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
|
||||
"A priority of 60 would make the objective work like a lowest priority order." +
|
||||
"So, if we'll want the character to wait, but still be able to find safety, defend themselves when attacked, or flee from dangers," +
|
||||
"it's better to use e.g. 70 instead of 100.")]
|
||||
public float Priority
|
||||
{
|
||||
get => _priority;
|
||||
set => _priority = Math.Clamp(value, AIObjectiveManager.LowestOrderPriority, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
|
||||
private float _priority;
|
||||
|
||||
private bool isFinished = false;
|
||||
|
||||
@@ -25,7 +40,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (isFinished) { return; }
|
||||
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character);
|
||||
affectedNpcs = ParentEvent.GetTargets(NPCTag).OfType<Character>();
|
||||
|
||||
foreach (var npc in affectedNpcs)
|
||||
{
|
||||
@@ -38,7 +53,7 @@ namespace Barotrauma
|
||||
AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true)
|
||||
{
|
||||
FaceTargetOnCompleted = false,
|
||||
OverridePriority = 100.0f,
|
||||
OverridePriority = Priority,
|
||||
SourceEventAction = this,
|
||||
IsWaitOrder = true,
|
||||
CloseEnough = 100
|
||||
|
||||
@@ -212,13 +212,24 @@ namespace Barotrauma
|
||||
{
|
||||
npc.CampaignInteractionType = CampaignMode.InteractionType.Examine;
|
||||
npc.RequireConsciousnessForCustomInteract = DisableIfTargetIncapacitated;
|
||||
#if CLIENT
|
||||
npc.SetCustomInteract(
|
||||
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
|
||||
(Character npc, Character interactor) =>
|
||||
{
|
||||
//the first character in the CustomInteract callback is always the NPC and the 2nd the character who interacted with it
|
||||
//but the TriggerAction can configure the 1st and 2nd entity in either order,
|
||||
//let's make sure we pass the NPC and the interactor in the intended order
|
||||
if (e1 == npc && ParentEvent.GetTargets(Target2Tag).Contains(interactor))
|
||||
{
|
||||
Trigger(npc, interactor);
|
||||
}
|
||||
else if (ParentEvent.GetTargets(Target1Tag).Contains(interactor) && e2 == npc)
|
||||
{
|
||||
Trigger(interactor, npc);
|
||||
}
|
||||
},
|
||||
#if CLIENT
|
||||
TextManager.GetWithVariable("CampaignInteraction.Examine", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)));
|
||||
#else
|
||||
npc.SetCustomInteract(
|
||||
(speaker, player) => { if (e1 == speaker) { Trigger(speaker, player); } else { Trigger(player, speaker); } },
|
||||
TextManager.Get("CampaignInteraction.Talk"));
|
||||
GameMain.NetworkMember.CreateEntityEvent(npc, new Character.AssignCampaignInteractionEventData());
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -9,6 +10,13 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
class UnlockPathAction : EventAction
|
||||
{
|
||||
private static readonly HashSet<LocationConnection> pathsUnlockedThisRound = new HashSet<LocationConnection>();
|
||||
|
||||
public static void ResetPathsUnlockedThisRound()
|
||||
{
|
||||
pathsUnlockedThisRound.Clear();
|
||||
}
|
||||
|
||||
public UnlockPathAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
|
||||
|
||||
private bool isFinished = false;
|
||||
@@ -32,6 +40,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!connection.Locked) { continue; }
|
||||
connection.Locked = false;
|
||||
pathsUnlockedThisRound.Add(connection);
|
||||
#if SERVER
|
||||
NotifyUnlock(connection);
|
||||
#else
|
||||
@@ -50,17 +59,30 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
private void NotifyUnlock(LocationConnection connection)
|
||||
public static void NotifyPathsUnlockedThisRound(Client client)
|
||||
{
|
||||
foreach (LocationConnection connection in pathsUnlockedThisRound)
|
||||
{
|
||||
NotifyUnlock(connection, client);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyUnlock(LocationConnection connection)
|
||||
{
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
IWriteMessage outmsg = new WriteOnlyMessage();
|
||||
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
|
||||
outmsg.WriteByte((byte)EventManager.NetworkEventType.UNLOCKPATH);
|
||||
outmsg.WriteUInt16((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection));
|
||||
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
NotifyUnlock(connection, client);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NotifyUnlock(LocationConnection connection, Client client)
|
||||
{
|
||||
IWriteMessage outmsg = new WriteOnlyMessage();
|
||||
outmsg.WriteByte((byte)ServerPacketHeader.EVENTACTION);
|
||||
outmsg.WriteByte((byte)EventManager.NetworkEventType.UNLOCKPATH);
|
||||
outmsg.WriteUInt16((UInt16)GameMain.GameSession.Map.Connections.IndexOf(connection));
|
||||
GameMain.Server.ServerPeer.Send(outmsg, client.Connection, DeliveryMethod.Reliable);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -158,6 +158,7 @@ namespace Barotrauma
|
||||
activeEvents.Clear();
|
||||
#if SERVER
|
||||
MissionAction.ResetMissionsUnlockedThisRound();
|
||||
UnlockPathAction.ResetPathsUnlockedThisRound();
|
||||
#endif
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList, false);
|
||||
totalPathLength = 0.0f;
|
||||
@@ -187,6 +188,12 @@ namespace Barotrauma
|
||||
var selectAlwaysEventSets = GetAllowedEventSets(EventSet.Prefabs.ToList(), requireCampaignSet: playingCampaign).Where(s => s.SelectAlways);
|
||||
foreach (var eventSet in selectAlwaysEventSets)
|
||||
{
|
||||
if (eventSet.GetCommonness(level) <= 0.0f)
|
||||
{
|
||||
//you might be wondering why an event set would be configured to SelectAlways, but have a commonness of 0:
|
||||
//the set might have a non-zero commonness in some other biome or level type, but not this one
|
||||
continue;
|
||||
}
|
||||
if (eventSet.Additive)
|
||||
{
|
||||
additiveSet = eventSet;
|
||||
@@ -252,6 +259,22 @@ namespace Barotrauma
|
||||
level.StartLocation.Connections.ForEach(c => c.Locked = false);
|
||||
}
|
||||
}
|
||||
if (GameMain.NetworkMember is not { IsClient: true } && level.StartOutpost != null)
|
||||
{
|
||||
foreach (var eventTag in level.StartOutpost.Info.TriggerOutpostMissionEvents)
|
||||
{
|
||||
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(identifier: Identifier.Empty, tag: eventTag, level.StartOutpost.ContentPackage);
|
||||
if (eventPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Outpost {level.StartOutpost.Info.DisplayName} failed to trigger an event (tag: {eventTag}).", contentPackage: level.StartOutpost.ContentPackage);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newEvent = eventPrefab.CreateInstance(RandomSeed);
|
||||
ActivateEvent(newEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RegisterNonRepeatableChildEvents(initialEventSet);
|
||||
void RegisterNonRepeatableChildEvents(EventSet eventSet)
|
||||
@@ -450,12 +473,10 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Registers the exhaustible events in the level as exhausted, and adds the current events to the event history
|
||||
/// </summary>
|
||||
public void RegisterEventHistory(bool registerFinishedOnly = false)
|
||||
public void StoreEventDataAtRoundEnd(bool registerFinishedOnly = false)
|
||||
{
|
||||
if (level?.LevelData == null) { return; }
|
||||
|
||||
level.LevelData.EventsExhausted = !registerFinishedOnly;
|
||||
|
||||
if (level.LevelData.Type == LevelData.LevelType.Outpost)
|
||||
{
|
||||
if (registerFinishedOnly)
|
||||
@@ -466,7 +487,7 @@ namespace Barotrauma
|
||||
if (parentSet == null) { continue; }
|
||||
if (parentSet.Exhaustible)
|
||||
{
|
||||
level.LevelData.EventsExhausted = true;
|
||||
level.LevelData.ExhaustEventSet(parentSet);
|
||||
}
|
||||
if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1))
|
||||
{
|
||||
@@ -513,7 +534,7 @@ namespace Barotrauma
|
||||
selectedEvents.Remove(eventSet);
|
||||
if (level == null) { return; }
|
||||
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
|
||||
if (eventSet.Exhaustible && level.LevelData.EventsExhausted) { return; }
|
||||
if (eventSet.Exhaustible && level.LevelData.IsEventSetExhausted(eventSet)) { return; }
|
||||
|
||||
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
|
||||
|
||||
@@ -740,7 +761,7 @@ namespace Barotrauma
|
||||
{
|
||||
return
|
||||
level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) &&
|
||||
level.LevelData.Type == eventSet.LevelType &&
|
||||
eventSet.LevelType.HasFlag(level.LevelData.Type) &&
|
||||
(eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) &&
|
||||
(eventSet.RequiredSpawnPointTag.IsEmpty || WayPoint.WayPointList.Any(wp => wp.Tags.Contains(eventSet.RequiredSpawnPointTag))) &&
|
||||
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
|
||||
@@ -753,7 +774,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (eventSet.Faction != location.Faction?.Prefab.Identifier && eventSet.Faction != location.SecondaryFaction?.Prefab.Identifier) { return false; }
|
||||
}
|
||||
var locationType = location.GetLocationType();
|
||||
var locationType = location.Type;
|
||||
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
|
||||
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
|
||||
return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier);
|
||||
@@ -1148,16 +1169,15 @@ namespace Barotrauma
|
||||
/// Get the entity that should be used in determining how far the player has progressed in the level.
|
||||
/// = The submarine or player character that has progressed the furthest.
|
||||
/// </summary>
|
||||
public static ISpatialEntity GetRefEntity()
|
||||
public static ISpatialEntity GetRefEntity(bool acceptRemoteControlledSubs = false)
|
||||
{
|
||||
ISpatialEntity refEntity = Submarine.MainSub;
|
||||
#if CLIENT
|
||||
if (Character.Controlled != null)
|
||||
{
|
||||
if (Character.Controlled.Submarine != null &&
|
||||
Character.Controlled.Submarine.Info.Type == SubmarineType.Player)
|
||||
if (Character.Controlled.Submarine is { Info.Type: SubmarineType.Player } playerSub)
|
||||
{
|
||||
refEntity = Character.Controlled.Submarine;
|
||||
GetRefSubForCharacter(Character.Controlled);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1170,19 +1190,39 @@ namespace Barotrauma
|
||||
foreach (Barotrauma.Networking.Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (client.Character == null) { continue; }
|
||||
//only take the players inside a player sub into account.
|
||||
//Otherwise the system could be abused by for example making a respawned player wait
|
||||
//close to the destination outpost
|
||||
if (client.Character.Submarine != null &&
|
||||
client.Character.Submarine.Info.Type == SubmarineType.Player)
|
||||
GetRefSubForCharacter(client.Character);
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
void GetRefSubForCharacter(Character character)
|
||||
{
|
||||
if (character.Submarine is { Info.Type: SubmarineType.Player } playerSub)
|
||||
{
|
||||
if (client.Character.Submarine.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
if (playerSub.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
{
|
||||
refEntity = client.Character.Submarine;
|
||||
refEntity = playerSub;
|
||||
}
|
||||
}
|
||||
if (acceptRemoteControlledSubs)
|
||||
{
|
||||
if (character.ViewTarget?.Submarine is { Info.Type: SubmarineType.Player } viewedSub)
|
||||
{
|
||||
if (viewedSub.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
{
|
||||
refEntity = viewedSub;
|
||||
}
|
||||
}
|
||||
if (character.SelectedItem?.GetComponent<Steering>()?.ControlledSub is { } controlledSub)
|
||||
{
|
||||
if (controlledSub.WorldPosition.X > refEntity.WorldPosition.X)
|
||||
{
|
||||
refEntity = controlledSub;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return refEntity;
|
||||
}
|
||||
|
||||
|
||||
+41
-170
@@ -1,6 +1,5 @@
|
||||
using Barotrauma.Extensions;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
@@ -9,10 +8,6 @@ namespace Barotrauma
|
||||
{
|
||||
partial class AbandonedOutpostMission : Mission
|
||||
{
|
||||
private readonly XElement characterConfig;
|
||||
|
||||
protected readonly List<Character> characters = new List<Character>();
|
||||
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
|
||||
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
|
||||
|
||||
@@ -82,8 +77,6 @@ namespace Barotrauma
|
||||
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
|
||||
base(prefab, locations, sub)
|
||||
{
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
|
||||
allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true);
|
||||
|
||||
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
|
||||
@@ -97,8 +90,6 @@ namespace Barotrauma
|
||||
{
|
||||
failed = false;
|
||||
endTimer = 0.0f;
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
requireKill.Clear();
|
||||
requireRescue.Clear();
|
||||
items.Clear();
|
||||
@@ -165,165 +156,6 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitCharacters(Submarine submarine)
|
||||
{
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
|
||||
if (characterConfig != null)
|
||||
{
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
|
||||
|
||||
int defaultCount = element.GetAttributeInt("count", -1);
|
||||
if (defaultCount < 0)
|
||||
{
|
||||
defaultCount = element.GetAttributeInt("amount", 1);
|
||||
}
|
||||
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
|
||||
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
|
||||
int count = Rand.Range(min, max + 1);
|
||||
|
||||
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
|
||||
{
|
||||
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
|
||||
if (humanPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadHuman(humanPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty));
|
||||
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (characterPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadMonster(characterPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
|
||||
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
|
||||
var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
|
||||
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
|
||||
SpawnAction.SpawnLocationType.Outpost, spawnPointType,
|
||||
moduleFlags ?? humanPrefab.GetModuleFlags(),
|
||||
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
|
||||
element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
|
||||
var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, originalTeam, spawnPos);
|
||||
//consider the NPC to be "originally" from the team of the outpost it spawns in, and change it to the desired (hostile) team afterwards
|
||||
//that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost
|
||||
if (teamId != originalTeam)
|
||||
{
|
||||
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
|
||||
}
|
||||
if (element.GetAttribute("color") != null)
|
||||
{
|
||||
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
|
||||
}
|
||||
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
|
||||
foreach (Identifier tag in humanPrefab.GetTags())
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
|
||||
}
|
||||
}
|
||||
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
spawnedCharacter.GiveIdCardTags(wp);
|
||||
}
|
||||
|
||||
if (requiresRescue)
|
||||
{
|
||||
requireRescue.Add(spawnedCharacter);
|
||||
#if CLIENT
|
||||
if (allowOrderingRescuees)
|
||||
{
|
||||
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi)
|
||||
{
|
||||
var order = OrderPrefab.Prefabs["fightintruders"]
|
||||
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
|
||||
}
|
||||
InitCharacter(spawnedCharacter, element);
|
||||
}
|
||||
|
||||
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
|
||||
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
|
||||
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
|
||||
characters.Add(spawnedCharacter);
|
||||
if (spawnedCharacter.Inventory != null)
|
||||
{
|
||||
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
|
||||
}
|
||||
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(submarine);
|
||||
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
|
||||
foreach (Submarine sub in Submarine.MainSub.DockedTo)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(sub);
|
||||
}
|
||||
}
|
||||
InitCharacter(spawnedCharacter, element);
|
||||
}
|
||||
|
||||
private void InitCharacter(Character character, XElement element)
|
||||
{
|
||||
if (element.GetAttributeBool("requirekill", false))
|
||||
{
|
||||
requireKill.Add(character);
|
||||
}
|
||||
float playDeadProbability = element.GetAttributeFloat("playdeadprobability", -1);
|
||||
if (playDeadProbability >= 0)
|
||||
{
|
||||
character.EvaluatePlayDeadProbability(playDeadProbability);
|
||||
}
|
||||
float huskProbability = element.GetAttributeFloat("huskprobability", 0);
|
||||
if (huskProbability > 0 && Rand.Value() <= huskProbability)
|
||||
{
|
||||
character.TurnIntoHusk();
|
||||
}
|
||||
else if (element.GetAttributeBool("corpse", false))
|
||||
{
|
||||
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
@@ -341,7 +173,7 @@ namespace Barotrauma
|
||||
if (endTimer > EndDelay)
|
||||
{
|
||||
#if SERVER
|
||||
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
|
||||
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
|
||||
{
|
||||
GameMain.Server.EndGame();
|
||||
}
|
||||
@@ -362,7 +194,7 @@ namespace Barotrauma
|
||||
break;
|
||||
#if SERVER
|
||||
case 1:
|
||||
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
|
||||
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
|
||||
{
|
||||
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
|
||||
{
|
||||
@@ -385,5 +217,44 @@ namespace Barotrauma
|
||||
{
|
||||
failed = !completed && requireRescue.Any(r => r.Removed || r.IsDead);
|
||||
}
|
||||
|
||||
protected override void InitCharacter(Character character, XElement element)
|
||||
{
|
||||
base.InitCharacter(character, element);
|
||||
if (element.GetAttributeBool("requirekill", false))
|
||||
{
|
||||
requireKill.Add(character);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Character spawnedCharacter = base.LoadHuman(humanPrefab, element, submarine);
|
||||
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
|
||||
if (requiresRescue)
|
||||
{
|
||||
requireRescue.Add(spawnedCharacter);
|
||||
#if CLIENT
|
||||
if (allowOrderingRescuees)
|
||||
{
|
||||
GameMain.GameSession.CrewManager?.AddCharacterToCrewList(spawnedCharacter);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController)
|
||||
{
|
||||
var order = OrderPrefab.Prefabs["fightintruders"]
|
||||
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
|
||||
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
|
||||
}
|
||||
// Overrides the team change set in the base method.
|
||||
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
|
||||
if (teamId != spawnedCharacter.TeamID)
|
||||
{
|
||||
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
|
||||
}
|
||||
return spawnedCharacter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,13 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class EscortMission : Mission
|
||||
{
|
||||
private readonly ContentXElement characterConfig;
|
||||
private readonly ContentXElement itemConfig;
|
||||
|
||||
private readonly List<Character> characters = new List<Character>();
|
||||
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
|
||||
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
|
||||
|
||||
private readonly int baseEscortedCharacters;
|
||||
@@ -36,7 +32,6 @@ namespace Barotrauma
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
missionSub = sub;
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
|
||||
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
|
||||
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
|
||||
|
||||
@@ -161,6 +161,10 @@ namespace Barotrauma
|
||||
private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
|
||||
|
||||
public Action<Mission> OnMissionStateChanged;
|
||||
|
||||
protected readonly ContentXElement characterConfig;
|
||||
protected readonly List<Character> characters = new List<Character>();
|
||||
protected readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
|
||||
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
|
||||
{
|
||||
@@ -192,6 +196,8 @@ namespace Barotrauma
|
||||
messages[m] = ReplaceVariablesInMissionMessage(messages[m], sub);
|
||||
}
|
||||
Messages = messages.ToImmutableArray();
|
||||
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
}
|
||||
|
||||
public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true)
|
||||
@@ -262,6 +268,162 @@ namespace Barotrauma
|
||||
}
|
||||
return (int)Math.Round(reward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call to load character elements to be spawned. Has to be implemented (and synced) separately per each mission.
|
||||
/// </summary>
|
||||
protected void InitCharacters(Submarine submarine)
|
||||
{
|
||||
characters.Clear();
|
||||
characterItems.Clear();
|
||||
|
||||
if (characterConfig != null)
|
||||
{
|
||||
foreach (XElement element in characterConfig.Elements())
|
||||
{
|
||||
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
|
||||
|
||||
int defaultCount = element.GetAttributeInt("count", -1);
|
||||
if (defaultCount < 0)
|
||||
{
|
||||
defaultCount = element.GetAttributeInt("amount", 1);
|
||||
}
|
||||
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
|
||||
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
|
||||
int count = Rand.Range(min, max + 1);
|
||||
|
||||
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
|
||||
{
|
||||
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
|
||||
if (humanPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a human character for a mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadHuman(humanPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty));
|
||||
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
||||
if (characterPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"Couldn't spawn a character for a mission: character prefab \"{speciesName}\" not found",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
LoadMonster(characterPrefab, element, submarine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SpawnAction.SpawnLocationType GetSpawnLocationTypeFromSubmarineType(Submarine sub)
|
||||
{
|
||||
return sub.Info.Type switch
|
||||
{
|
||||
SubmarineType.Outpost or SubmarineType.OutpostModule => SpawnAction.SpawnLocationType.Outpost,
|
||||
SubmarineType.Wreck => SpawnAction.SpawnLocationType.Wreck,
|
||||
SubmarineType.Ruin => SpawnAction.SpawnLocationType.Ruin,
|
||||
SubmarineType.BeaconStation => SpawnAction.SpawnLocationType.BeaconStation,
|
||||
SubmarineType.Player => SpawnAction.SpawnLocationType.MainSub,
|
||||
_ => SpawnAction.SpawnLocationType.Any
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
|
||||
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
|
||||
var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
|
||||
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
|
||||
GetSpawnLocationTypeFromSubmarineType(submarine), spawnPointType,
|
||||
moduleFlags ?? humanPrefab.GetModuleFlags(),
|
||||
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
|
||||
element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
var teamId = element.GetAttributeEnum("teamid", CharacterTeamType.None);
|
||||
var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
|
||||
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, originalTeam, spawnPos);
|
||||
//consider the NPC to be "originally" from the team of the outpost it spawns in, and change it to the desired (hostile) team afterwards
|
||||
//that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost
|
||||
if (teamId != originalTeam)
|
||||
{
|
||||
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId, processImmediately: true);
|
||||
}
|
||||
if (element.GetAttribute("color") != null)
|
||||
{
|
||||
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
|
||||
}
|
||||
if (submarine.Info is { IsOutpost: true } outPostInfo)
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
|
||||
foreach (Identifier tag in humanPrefab.GetTags())
|
||||
{
|
||||
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
|
||||
}
|
||||
}
|
||||
if (spawnPos is WayPoint wp)
|
||||
{
|
||||
spawnedCharacter.GiveIdCardTags(wp);
|
||||
}
|
||||
InitCharacter(spawnedCharacter, element);
|
||||
return spawnedCharacter;
|
||||
}
|
||||
|
||||
protected virtual Character LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
|
||||
{
|
||||
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
|
||||
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
|
||||
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
|
||||
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
|
||||
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
|
||||
characters.Add(spawnedCharacter);
|
||||
if (spawnedCharacter.Inventory != null)
|
||||
{
|
||||
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
|
||||
}
|
||||
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(submarine);
|
||||
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
|
||||
foreach (Submarine sub in Submarine.MainSub.DockedTo)
|
||||
{
|
||||
enemyAi.UnattackableSubmarines.Add(sub);
|
||||
}
|
||||
}
|
||||
InitCharacter(spawnedCharacter, element);
|
||||
return spawnedCharacter;
|
||||
}
|
||||
|
||||
protected virtual void InitCharacter(Character character, XElement element)
|
||||
{
|
||||
if (element.GetAttributeBool(Tags.IgnoredByAI.Value, false))
|
||||
{
|
||||
character.AddAbilityFlag(AbilityFlags.IgnoredByEnemyAI);
|
||||
}
|
||||
float playDeadProbability = element.GetAttributeFloat("playdeadprobability", -1);
|
||||
if (playDeadProbability >= 0)
|
||||
{
|
||||
character.EvaluatePlayDeadProbability(playDeadProbability);
|
||||
}
|
||||
float huskProbability = element.GetAttributeFloat("huskprobability", 0);
|
||||
if (huskProbability > 0 && Rand.Value() <= huskProbability)
|
||||
{
|
||||
character.TurnIntoHusk();
|
||||
}
|
||||
else if (element.GetAttributeBool("corpse", false))
|
||||
{
|
||||
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void Start(Level level)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace Barotrauma
|
||||
partial class PirateMission : Mission
|
||||
{
|
||||
private readonly ContentXElement submarineTypeConfig;
|
||||
private readonly ContentXElement characterConfig;
|
||||
private readonly ContentXElement characterTypeConfig;
|
||||
private readonly float addedMissionDifficultyPerPlayer;
|
||||
|
||||
@@ -22,8 +21,8 @@ namespace Barotrauma
|
||||
private Identifier factionIdentifier;
|
||||
|
||||
private Submarine enemySub;
|
||||
private readonly List<Character> characters = new List<Character>();
|
||||
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
|
||||
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
|
||||
|
||||
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
|
||||
|
||||
@@ -94,7 +93,6 @@ namespace Barotrauma
|
||||
public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
submarineTypeConfig = prefab.ConfigElement.GetChildElement("SubmarineTypes");
|
||||
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
|
||||
characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes");
|
||||
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@ namespace Barotrauma
|
||||
public readonly Identifier ContainerTag;
|
||||
public readonly Identifier ExistingItemTag;
|
||||
|
||||
/// <summary>
|
||||
/// If true, target location indicator points to the submarine where the target is inside when the target is not yet found. Not used, if target is not inside any submarine.
|
||||
/// When enabled, the indicator is hidden when the player is inside the target submarine.
|
||||
/// </summary>
|
||||
public readonly bool PointToSub;
|
||||
|
||||
public readonly bool RemoveItem;
|
||||
|
||||
public readonly LocalizedString SonarLabel;
|
||||
@@ -55,6 +61,8 @@ namespace Barotrauma
|
||||
public readonly RetrievalState RequiredRetrievalState;
|
||||
|
||||
public readonly bool HideLabelAfterRetrieved;
|
||||
public readonly bool HideLabelWhenFound;
|
||||
public readonly bool HideLabelWhenNotFound;
|
||||
|
||||
public bool Retrieved
|
||||
{
|
||||
@@ -115,6 +123,9 @@ namespace Barotrauma
|
||||
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", parentTarget?.RequiredRetrievalState ?? RetrievalState.RetrievedToSub);
|
||||
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", parentTarget != null);
|
||||
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", parentTarget?.HideLabelAfterRetrieved ?? false);
|
||||
HideLabelWhenFound = element.GetAttributeBool(nameof(HideLabelWhenFound), parentTarget?.HideLabelWhenFound ?? false);
|
||||
HideLabelWhenNotFound = element.GetAttributeBool(nameof(HideLabelWhenNotFound), parentTarget?.HideLabelWhenNotFound ?? false);
|
||||
PointToSub = element.GetAttributeBool(nameof(PointToSub), parentTarget?.PointToSub ?? false);
|
||||
RequireInsideOriginalContainer = element.GetAttributeBool("requireinsideoriginalcontainer", false);
|
||||
|
||||
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
|
||||
@@ -203,6 +214,8 @@ namespace Barotrauma
|
||||
/// What percentage of targets need to be retrieved for the mission to complete (0.0 - 1.0). Defaults to 0.98.
|
||||
/// </summary>
|
||||
private readonly float requiredDeliveryAmount;
|
||||
|
||||
private LocalizedString pickedUpMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Message displayed when at least one of the targets is retrieved, but the mission is not complete yet.
|
||||
@@ -225,8 +238,26 @@ namespace Barotrauma
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; }
|
||||
if (target.Item != null && !target.Item.Removed)
|
||||
if (target.State is Target.RetrievalState.None)
|
||||
{
|
||||
if (target.HideLabelWhenNotFound) { continue; }
|
||||
}
|
||||
else if (target.HideLabelWhenFound)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (target.Item is { Removed: false })
|
||||
{
|
||||
if (target.PointToSub && target.Item.Submarine is Submarine targetSub && target.State == Target.RetrievalState.None)
|
||||
{
|
||||
if (Character.Controlled is Character playerCharacter && playerCharacter.Submarine != targetSub)
|
||||
{
|
||||
// The target is not in the same sub as the player -> point to the target submarine (instead of the item position).
|
||||
// When inside the target sub, don't show anything.
|
||||
yield return (target.SonarLabel, targetSub.WorldPosition);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (target.Item.ParentInventory?.Owner is Item parentItem)
|
||||
{
|
||||
bool insideParentItem = false;
|
||||
@@ -238,7 +269,7 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
//if the item is inside another target that has it's own sonar label, no need to show one on this item
|
||||
//if the item is inside another target that has its own sonar label, no need to show one on this item
|
||||
if (insideParentItem) { continue; }
|
||||
}
|
||||
|
||||
@@ -263,6 +294,7 @@ namespace Barotrauma
|
||||
|
||||
partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage));
|
||||
allRetrievedMessage = GetMessage(nameof(allRetrievedMessage));
|
||||
pickedUpMessage = GetMessage(nameof(pickedUpMessage));
|
||||
|
||||
foreach (ContentXElement subElement in prefab.ConfigElement.Elements())
|
||||
{
|
||||
@@ -341,6 +373,17 @@ namespace Barotrauma
|
||||
#if SERVER
|
||||
spawnInfo.Clear();
|
||||
#endif
|
||||
if (!IsClient)
|
||||
{
|
||||
// First spawn any possible characters, so that we can use their items as targets.
|
||||
Target firstTarget = targets.First();
|
||||
var submarine = Submarine.Loaded.Find(s => IsValidSubmarine(s, firstTarget.SpawnPositionType));
|
||||
if (submarine != null)
|
||||
{
|
||||
InitCharacters(submarine);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var target in targets)
|
||||
{
|
||||
bool usedExistingItem = false;
|
||||
@@ -380,39 +423,36 @@ namespace Barotrauma
|
||||
case Level.PositionType.Cave:
|
||||
case Level.PositionType.MainPath:
|
||||
case Level.PositionType.SidePath:
|
||||
case Level.PositionType.AbyssCave:
|
||||
target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
|
||||
#if SERVER
|
||||
usedExistingItem = target.Item != null;
|
||||
#endif
|
||||
break;
|
||||
case Level.PositionType.Abyss:
|
||||
target.Item = suitableItems.FirstOrDefault(it => Level.IsPositionInAbyss(it.WorldPosition));
|
||||
break;
|
||||
case Level.PositionType.Ruin:
|
||||
case Level.PositionType.Wreck:
|
||||
case Level.PositionType.Outpost:
|
||||
case Level.PositionType.BeaconStation:
|
||||
foreach (Item it in suitableItems)
|
||||
{
|
||||
if (it.Submarine?.Info == null) { continue; }
|
||||
if (target.SpawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; }
|
||||
if (target.SpawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
|
||||
if (target.SpawnPositionType == Level.PositionType.Outpost && it.Submarine.Info.Type != SubmarineType.Outpost) { continue; }
|
||||
Rectangle worldBorders = it.Submarine.Borders;
|
||||
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
|
||||
if (it.Submarine is not Submarine sub) { continue; }
|
||||
if (!IsValidSubmarine(sub, target.SpawnPositionType)) { continue; }
|
||||
Rectangle worldBorders = sub.Borders;
|
||||
worldBorders.Location += sub.WorldPosition.ToPoint();
|
||||
if (Submarine.RectContains(worldBorders, it.WorldPosition))
|
||||
{
|
||||
target.Item = it;
|
||||
#if SERVER
|
||||
usedExistingItem = true;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
target.Item = suitableItems.FirstOrDefault();
|
||||
#if SERVER
|
||||
usedExistingItem = target.Item != null;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
#if SERVER
|
||||
usedExistingItem = target.Item != null;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (target.Item == null)
|
||||
@@ -464,19 +504,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (!it.HasTag(target.ContainerTag)) { continue; }
|
||||
if (!it.IsPlayerTeamInteractable) { continue; }
|
||||
switch (target.SpawnPositionType)
|
||||
{
|
||||
case Level.PositionType.Cave:
|
||||
case Level.PositionType.MainPath:
|
||||
if (it.Submarine != null) { continue; }
|
||||
break;
|
||||
case Level.PositionType.Ruin:
|
||||
if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; }
|
||||
break;
|
||||
case Level.PositionType.Wreck:
|
||||
if (it.Submarine?.Info == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
|
||||
break;
|
||||
}
|
||||
if (!IsValidSubmarine(it.Submarine, target.SpawnPositionType)) { continue; }
|
||||
var itemContainer = it.GetComponent<ItemContainer>();
|
||||
if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); }
|
||||
}
|
||||
@@ -538,6 +566,26 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidSubmarine(Submarine sub, Level.PositionType spawnPosType)
|
||||
{
|
||||
if (sub == null)
|
||||
{
|
||||
return spawnPosType switch
|
||||
{
|
||||
Level.PositionType.Ruin or Level.PositionType.Wreck or Level.PositionType.BeaconStation or Level.PositionType.Outpost => false,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
return spawnPosType switch
|
||||
{
|
||||
Level.PositionType.Ruin => sub.Info.IsRuin,
|
||||
Level.PositionType.Wreck => sub.Info.IsWreck,
|
||||
Level.PositionType.BeaconStation => sub.Info.IsBeacon,
|
||||
Level.PositionType.Outpost => sub.Info.IsOutpost,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
protected override void UpdateMissionSpecific(float deltaTime)
|
||||
{
|
||||
@@ -571,48 +619,45 @@ namespace Barotrauma
|
||||
switch (target.State)
|
||||
{
|
||||
case Target.RetrievalState.None:
|
||||
if (target.Interacted)
|
||||
{
|
||||
if (target.Interacted)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.Interact);
|
||||
}
|
||||
var root = target.Item?.RootContainer ?? target.Item;
|
||||
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.PickedUp);
|
||||
}
|
||||
if (inPlayerSub)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
TrySetRetrievalState(Target.RetrievalState.Interact);
|
||||
}
|
||||
var root = target.Item?.RootContainer ?? target.Item;
|
||||
if (root.ParentInventory?.Owner is Character { TeamID: CharacterTeamType.Team1 })
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.PickedUp);
|
||||
#if CLIENT
|
||||
TryShowPickedUpMessage();
|
||||
#endif
|
||||
}
|
||||
if (inPlayerSub)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
break;
|
||||
case Target.RetrievalState.PickedUp:
|
||||
case Target.RetrievalState.RetrievedToSub:
|
||||
bool inPlayerInventory = false;
|
||||
bool playerInFriendlySub = false;
|
||||
if (rootInventoryOwner is Character { TeamID: CharacterTeamType.Team1 } character)
|
||||
{
|
||||
|
||||
bool inPlayerInventory = false;
|
||||
bool playerInFriendlySub = false;
|
||||
if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1)
|
||||
inPlayerInventory = true;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
inPlayerInventory = true;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
playerInFriendlySub =
|
||||
character.IsInFriendlySub ||
|
||||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.State = Target.RetrievalState.PickedUp;
|
||||
playerInFriendlySub =
|
||||
character.IsInFriendlySub ||
|
||||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
|
||||
}
|
||||
}
|
||||
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.State = Target.RetrievalState.PickedUp;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -621,7 +666,7 @@ namespace Barotrauma
|
||||
if (retrievalState < target.State || target.State == retrievalState) { return; }
|
||||
bool wasRetrieved = target.Retrieved;
|
||||
target.State = retrievalState;
|
||||
//increment the mission state if the target became retrieved
|
||||
//increment the mission state if the target became retrieved
|
||||
if (!wasRetrieved && target.Retrieved)
|
||||
{
|
||||
State = Math.Max(i + 1, State);
|
||||
@@ -645,7 +690,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (requiredDeliveryAmount < 1.0f)
|
||||
{
|
||||
return targets.Count(t => IsTargetRetrieved(t)) / (float)targets.Count >= requiredDeliveryAmount;
|
||||
return targets.Count(IsTargetRetrieved) / (float)targets.Count >= requiredDeliveryAmount;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -679,7 +724,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (var target in targetsToRemove)
|
||||
{
|
||||
if (target.Item != null && !target.Item.Removed)
|
||||
if (target.Item is { Removed: false })
|
||||
{
|
||||
target.Item.Remove();
|
||||
}
|
||||
|
||||
@@ -160,9 +160,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static Submarine GetReferenceSub()
|
||||
private static Submarine GetReferenceSub(bool acceptRemoteControlledSubs)
|
||||
{
|
||||
return EventManager.GetRefEntity() as Submarine ?? Submarine.MainSub;
|
||||
return EventManager.GetRefEntity(acceptRemoteControlledSubs) as Submarine ?? Submarine.MainSub;
|
||||
}
|
||||
|
||||
public override IEnumerable<ContentFile> GetFilesToPreload()
|
||||
@@ -223,14 +223,6 @@ namespace Barotrauma
|
||||
{
|
||||
createdCharacter.EvaluatePlayDeadProbability(overridePlayDeadProbability);
|
||||
}
|
||||
if (GameMain.GameSession.IsCurrentLocationRadiated())
|
||||
{
|
||||
AfflictionPrefab radiationPrefab = AfflictionPrefab.RadiationSickness;
|
||||
Affliction affliction = new Affliction(radiationPrefab, radiationPrefab.MaxStrength);
|
||||
createdCharacter?.CharacterHealth.ApplyAffliction(null, affliction);
|
||||
// TODO test multiplayer
|
||||
createdCharacter?.Kill(CauseOfDeathType.Affliction, affliction, log: false);
|
||||
}
|
||||
createdCharacter.DisabledByEvent = true;
|
||||
monsters.Add(createdCharacter);
|
||||
}
|
||||
@@ -307,11 +299,18 @@ namespace Barotrauma
|
||||
disallowed = true;
|
||||
return;
|
||||
}
|
||||
Submarine refSub = GetReferenceSub();
|
||||
Submarine refSub = GetReferenceSub(acceptRemoteControlledSubs: true);
|
||||
if (Submarine.MainSubs.Length == 2 && Submarine.MainSubs[1] != null)
|
||||
{
|
||||
refSub = Submarine.MainSubs.GetRandom(Rand.RandSync.Unsynced);
|
||||
}
|
||||
//if the reference sub is not the main sub, e.g. a remotely controlled drone,
|
||||
//there's a 50% chance that the monsters will spawn near the main sub instead
|
||||
//so you can't abuse the remotely controlled subs to make monsters only spawn somewhere far away from the main sub
|
||||
if (refSub != Submarine.MainSub && Rand.Range(0.0f, 1.0f) < 0.5f)
|
||||
{
|
||||
refSub ??= GetReferenceSub(acceptRemoteControlledSubs: false);
|
||||
}
|
||||
float closestDist = float.PositiveInfinity;
|
||||
//find the closest spawnposition that isn't too close to any of the subs
|
||||
foreach (var position in availablePositions)
|
||||
@@ -745,7 +744,9 @@ namespace Barotrauma
|
||||
DebugConsole.NewMessage($"Spawned: {ToString()}. Strength: {StringFormatter.FormatZeroDecimal(monsters.Sum(m => m.Params.AI?.CombatStrength ?? 0))}.", Color.LightBlue, debugOnly: true);
|
||||
}
|
||||
|
||||
if (GameMain.GameSession != null)
|
||||
if (GameMain.GameSession != null &&
|
||||
monster.ContentPackage == ContentPackageManager.VanillaCorePackage &&
|
||||
GameAnalyticsManager.ShouldLogRandomSample())
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(
|
||||
$"MonsterSpawn:{GameMain.GameSession.GameMode?.Preset?.Identifier.Value ?? "none"}:{Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"}:{SpawnPosType}:{SpeciesName}",
|
||||
|
||||
@@ -376,6 +376,33 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public enum DataSampleSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
Full
|
||||
}
|
||||
|
||||
private readonly static Dictionary<DataSampleSize, float> dataSampleSizes = new Dictionary<DataSampleSize, float>()
|
||||
{
|
||||
{ DataSampleSize.Small, 0.01f },
|
||||
{ DataSampleSize.Medium, 0.05f },
|
||||
{ DataSampleSize.Large, 0.5f },
|
||||
{ DataSampleSize.Full, 1.0f }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Should we log something into GameAnalytics if we only want a random sample of some events?
|
||||
/// Essentially just randomly decides whether to log or not based on the probability
|
||||
/// </summary>
|
||||
/// <param name="probability">A value between 0 and 1</param>
|
||||
/// <returns></returns>
|
||||
public static bool ShouldLogRandomSample(DataSampleSize sampleSize = DataSampleSize.Small)
|
||||
{
|
||||
return Rand.Range(0.0f, 1.0f) < dataSampleSizes[sampleSize];
|
||||
}
|
||||
|
||||
public static void AddErrorEvent(ErrorSeverity errorSeverity, string message)
|
||||
{
|
||||
if (!SendUserStatistics) { return; }
|
||||
|
||||
@@ -199,12 +199,12 @@ namespace Barotrauma
|
||||
{
|
||||
var itemPrefab = prefabsItemsCanSpawnIn[i];
|
||||
if (itemPrefab == null) { continue; }
|
||||
SpawnItems(itemPrefab);
|
||||
SpawnItems(itemPrefab, skipItemProbability);
|
||||
}
|
||||
|
||||
// Spawn items that nothing can spawn in last
|
||||
singlePrefabs.Shuffle(Rand.RandSync.ServerAndClient);
|
||||
singlePrefabs.ForEach(i => SpawnItems(i));
|
||||
singlePrefabs.ForEach(i => SpawnItems(i, skipItemProbability));
|
||||
|
||||
if (OutputDebugInfo)
|
||||
{
|
||||
@@ -251,7 +251,7 @@ namespace Barotrauma
|
||||
|
||||
return itemsToSpawn;
|
||||
|
||||
void SpawnItems(ItemPrefab itemPrefab, float skipItemProbability = 0.0f)
|
||||
void SpawnItems(ItemPrefab itemPrefab, float skipItemProbability)
|
||||
{
|
||||
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) < skipItemProbability) { return; }
|
||||
if (itemPrefab == null)
|
||||
|
||||
@@ -5,9 +5,9 @@ using Barotrauma.Tutorials;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -27,19 +27,29 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<CharacterInfo> characterInfos = new List<CharacterInfo>();
|
||||
private readonly List<Character> characters = new List<Character>();
|
||||
|
||||
private readonly List<CharacterInfo> reserveBench = new List<CharacterInfo>();
|
||||
|
||||
public IEnumerable<Character> GetCharacters()
|
||||
{
|
||||
return characters;
|
||||
}
|
||||
/// <summary>
|
||||
/// Note: this only returns AI characters' infos in multiplayer. The infos are used to manage hiring/firing/renaming, which only applies to AI characters.
|
||||
/// NOTE: When called from client code, this method will include players, but NOT when called from server code.
|
||||
/// CrewManager is used for dealing with things relevant to AI characters, like hiring, firing, renaming, and the reserve bench.
|
||||
/// In single player/client code, player CharacterInfos are still stored in it but only for displaying crew listings in the GUI correctly.
|
||||
/// Use <see cref="GetSessionCrewCharacters"/> to get all the characters regardless if they're player or AI controlled.
|
||||
/// </summary>
|
||||
public IEnumerable<CharacterInfo> GetCharacterInfos()
|
||||
/// <param name="includeReserveBench">Should characters on the reserve be included? Defaults to false.</param>
|
||||
public IEnumerable<CharacterInfo> GetCharacterInfos(bool includeReserveBench = false)
|
||||
{
|
||||
if (includeReserveBench)
|
||||
{
|
||||
return characterInfos.Concat(reserveBench);
|
||||
}
|
||||
return characterInfos;
|
||||
}
|
||||
|
||||
public IEnumerable<CharacterInfo> GetReserveBenchInfos()
|
||||
{
|
||||
return reserveBench;
|
||||
}
|
||||
|
||||
private Character welcomeMessageNPC;
|
||||
|
||||
@@ -174,7 +184,14 @@ namespace Barotrauma
|
||||
if (characterElement.GetAttributeBool("lastcontrolled", false)) { characterInfo.LastControlled = true; }
|
||||
characterInfo.CrewListIndex = characterElement.GetAttributeInt("crewlistindex", -1);
|
||||
#endif
|
||||
characterInfos.Add(characterInfo);
|
||||
if (characterElement.GetAttributeBool(nameof(CharacterInfo.IsOnReserveBench), false))
|
||||
{
|
||||
reserveBench.Add(characterInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
characterInfos.Add(characterInfo);
|
||||
}
|
||||
foreach (var subElement in characterElement.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -199,7 +216,14 @@ namespace Barotrauma
|
||||
/// <param name="characterInfo"></param>
|
||||
public void RemoveCharacterInfo(CharacterInfo characterInfo)
|
||||
{
|
||||
if (characterInfo is { IsOnReserveBench: true })
|
||||
{
|
||||
reserveBench.Remove(characterInfo);
|
||||
}
|
||||
characterInfos.Remove(characterInfo);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.DeathPrompt?.UpdateBotList();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void AddCharacter(Character character)
|
||||
@@ -289,6 +313,15 @@ namespace Barotrauma
|
||||
|
||||
public void AddCharacterInfo(CharacterInfo characterInfo)
|
||||
{
|
||||
if (GameMain.GameSession?.Campaign is MultiPlayerCampaign)
|
||||
{
|
||||
Debug.Assert(characterInfo.BotStatus == BotStatus.ActiveService);
|
||||
if (characterInfo.BotStatus != BotStatus.ActiveService)
|
||||
{
|
||||
DebugConsole.ThrowError($"CrewManager.AddCharacterInfo called on a bot ({characterInfo.DisplayName}) with the wrong status ({characterInfo.BotStatus})");
|
||||
}
|
||||
}
|
||||
|
||||
if (characterInfos.Contains(characterInfo))
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace.CleanupStackTrace());
|
||||
@@ -296,11 +329,15 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
characterInfos.Add(characterInfo);
|
||||
#if CLIENT
|
||||
GameMain.GameSession?.DeathPrompt?.UpdateBotList();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void ClearCharacterInfos()
|
||||
{
|
||||
characterInfos.Clear();
|
||||
reserveBench.Clear();
|
||||
}
|
||||
|
||||
public void InitRound()
|
||||
@@ -313,7 +350,7 @@ namespace Barotrauma
|
||||
|
||||
List<WayPoint> spawnWaypoints = null;
|
||||
List<WayPoint> mainSubWaypoints = WayPoint.SelectCrewSpawnPoints(characterInfos, Submarine.MainSub).ToList();
|
||||
|
||||
|
||||
if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
|
||||
{
|
||||
spawnWaypoints = GetOutpostSpawnpoints();
|
||||
@@ -324,7 +361,7 @@ namespace Barotrauma
|
||||
while (spawnWaypoints.Any() && spawnWaypoints.Count < characterInfos.Count)
|
||||
{
|
||||
spawnWaypoints.Add(spawnWaypoints[Rand.Int(spawnWaypoints.Count)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spawnWaypoints == null || !spawnWaypoints.Any())
|
||||
{
|
||||
|
||||
@@ -177,10 +177,28 @@ namespace Barotrauma
|
||||
|
||||
if (GameMain.NetworkMember.GameStarted)
|
||||
{
|
||||
//allow managing if no-one with permissions is alive and in-game
|
||||
return GameMain.NetworkMember.ConnectedClients.None(c =>
|
||||
c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } &&
|
||||
(IsOwner(c) || c.HasPermission(permissions)));
|
||||
bool someOneHasPermissions = GameMain.NetworkMember.ConnectedClients.Any(c => IsOwner(c) || c.HasPermission(permissions));
|
||||
if (someOneHasPermissions)
|
||||
{
|
||||
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration < 60.0f)
|
||||
{
|
||||
//round has been going on for less than a minute, don't allow anyone to manage just yet,
|
||||
//the people with permissions might still be loading or doing something in the lobby
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
//allow managing if the round has been going on for a while, and no-one with permissions is alive and in-game
|
||||
return GameMain.NetworkMember.ConnectedClients.None(c =>
|
||||
c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } &&
|
||||
(IsOwner(c) || c.HasPermission(permissions)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//no-one in the server with permissions, allow anyone to manage
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -963,7 +981,9 @@ namespace Barotrauma
|
||||
{
|
||||
UpdateStoreStock();
|
||||
}
|
||||
GameMain.GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
|
||||
|
||||
GameMain.GameSession.EndMissions();
|
||||
GameMain.GameSession.EventManager?.StoreEventDataAtRoundEnd(registerFinishedOnly: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1089,13 +1109,24 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var price = buyingNewCharacter ? NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo);
|
||||
int price = buyingNewCharacter ? NewCharacterCost(characterInfo) : HireManager.GetSalaryFor(characterInfo);
|
||||
if (takeMoney && !TryPurchase(client, price)) { return false; }
|
||||
|
||||
characterInfo.IsNewHire = true;
|
||||
characterInfo.Title = null;
|
||||
location.RemoveHireableCharacter(characterInfo);
|
||||
CrewManager.AddCharacterInfo(characterInfo);
|
||||
|
||||
if (GameMain.GameSession?.Campaign is MultiPlayerCampaign)
|
||||
{
|
||||
#if SERVER
|
||||
CrewManager.ToggleReserveBenchStatus(characterInfo, client, pendingHire: true, confirmPendingHire: true, sendUpdate: false);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
CrewManager.AddCharacterInfo(characterInfo);
|
||||
}
|
||||
|
||||
GameAnalyticsManager.AddMoneySpentEvent(characterInfo.Salary, GameAnalyticsManager.MoneySink.Crew, characterInfo.Job?.Prefab.Identifier.Value ?? "unknown");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -92,6 +92,11 @@ namespace Barotrauma
|
||||
get; set;
|
||||
}
|
||||
|
||||
public byte RoundID
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
private MultiPlayerCampaign(CampaignSettings settings) : base(GameModePreset.MultiPlayerCampaign, settings)
|
||||
{
|
||||
currentCampaignID++;
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (Submarine.MainSubs[1] is not null)
|
||||
if (Submarine.MainSubs[1] is not null && GameMain.GameSession.GameMode is PvPMode)
|
||||
{
|
||||
foreach (var team2Perk in Team2Perks)
|
||||
{
|
||||
@@ -703,70 +703,6 @@ namespace Barotrauma
|
||||
|
||||
casualties.Clear();
|
||||
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
GameAnalyticsManager.ProgressionStatus.Start,
|
||||
GameMode?.Preset?.Identifier.Value ?? "none");
|
||||
|
||||
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0));
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
|
||||
}
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
|
||||
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
|
||||
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId);
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"));
|
||||
#if CLIENT
|
||||
if (GameMode is TutorialMode tutorialMode)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
|
||||
if (GameMain.IsFirstLaunch)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
|
||||
}
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
|
||||
#endif
|
||||
var campaignMode = GameMode as CampaignMode;
|
||||
if (campaignMode != null)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount);
|
||||
//log the multipliers as integers to reduce the number of distinct values
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:FuelMultiplier:" + (int)(campaignMode.Settings.FuelMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:MissionRewardMultiplier:" + (int)(campaignMode.Settings.MissionRewardMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:CrewVitalityMultiplier:" + (int)(campaignMode.Settings.CrewVitalityMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:NonCrewVitalityMultiplier:" + (int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:OxygenMultiplier:" + (int)(campaignMode.Settings.OxygenMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShipyardPriceMultiplier:" + (int)(campaignMode.Settings.ShipyardPriceMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShopPriceMultiplier:" + (int)(campaignMode.Settings.ShopPriceMultiplier * 100));
|
||||
|
||||
bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome);
|
||||
if (firstTimeInBiome)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
|
||||
}
|
||||
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("ServerSettings:RespawnMode:" + serverSettings.RespawnMode);
|
||||
GameAnalyticsManager.AddDesignEvent("ServerSettings:IronmanMode:" + serverSettings.IronmanModeActive);
|
||||
GameAnalyticsManager.AddDesignEvent("ServerSettings:AllowBotTakeoverOnPermadeath:" + serverSettings.AllowBotTakeoverOnPermadeath);
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
double startDuration = (DateTime.Now - startTime).TotalSeconds;
|
||||
if (startDuration < MinimumLoadingTime)
|
||||
@@ -818,7 +754,11 @@ namespace Barotrauma
|
||||
|
||||
GameMain.LuaCs.Hook.Call("roundStart");
|
||||
EnableEventLogNotificationIcon(enabled: false);
|
||||
|
||||
LogStartRoundStats();
|
||||
|
||||
#endif
|
||||
var campaignMode = GameMode as CampaignMode;
|
||||
if (campaignMode is { ItemsRelocatedToMainSub: true })
|
||||
{
|
||||
#if SERVER
|
||||
@@ -1102,7 +1042,11 @@ namespace Barotrauma
|
||||
|
||||
#if SERVER
|
||||
players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead);
|
||||
bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer);
|
||||
bots = crewManager.GetCharacterInfos()
|
||||
//filter out players in case a player has been given control of a bot using console commands
|
||||
.Where(characterInfo => GameMain.Server.ConnectedClients.None(c => c.CharacterInfo == characterInfo))
|
||||
.Select(characterInfo => characterInfo.Character)
|
||||
.NotNull();
|
||||
#elif CLIENT
|
||||
players = crewManager.GetCharacters().Where(static c => c.IsPlayer);
|
||||
bots = crewManager.GetCharacters().Where(static c => c.IsBot);
|
||||
@@ -1124,7 +1068,7 @@ namespace Barotrauma
|
||||
private double LastEndRoundErrorMessageTime;
|
||||
#endif
|
||||
|
||||
public void EndRound(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null)
|
||||
public void EndRound(string endMessage, CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, TraitorManager.TraitorResults? traitorResults = null, bool createRoundSummary = true)
|
||||
{
|
||||
RoundEnding = true;
|
||||
|
||||
@@ -1141,35 +1085,14 @@ namespace Barotrauma
|
||||
|
||||
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
|
||||
int prevMoney = GetAmountOfMoney(crewCharacters);
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
mission.End();
|
||||
}
|
||||
|
||||
EndMissions();
|
||||
|
||||
foreach (Character character in crewCharacters)
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnRoundEnd);
|
||||
}
|
||||
|
||||
if (missions.Any())
|
||||
{
|
||||
if (missions.Any(m => m.Completed))
|
||||
{
|
||||
foreach (Character character in crewCharacters)
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
if (missions.All(m => m.Completed))
|
||||
{
|
||||
foreach (Character character in crewCharacters)
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnAllMissionsCompleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GameMain.LuaCs.Hook.Call("missionsEnded", missions);
|
||||
|
||||
#if CLIENT
|
||||
@@ -1186,7 +1109,10 @@ namespace Barotrauma
|
||||
|
||||
GUI.PreventPauseMenuToggle = true;
|
||||
|
||||
if (GameMode is not TestGameMode && Screen.Selected == GameMain.GameScreen && RoundSummary != null && transitionType != CampaignMode.TransitionType.End)
|
||||
if (createRoundSummary &&
|
||||
GameMode is not TestGameMode &&
|
||||
Screen.Selected == GameMain.GameScreen && RoundSummary != null &&
|
||||
transitionType != CampaignMode.TransitionType.End)
|
||||
{
|
||||
GUI.ClearMessages();
|
||||
GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData is RoundSummary);
|
||||
@@ -1265,6 +1191,34 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public void EndMissions()
|
||||
{
|
||||
ImmutableHashSet<Character> crewCharacters = GetSessionCrewCharacters(CharacterType.Both);
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
mission.End();
|
||||
}
|
||||
|
||||
if (missions.Any())
|
||||
{
|
||||
if (missions.Any(m => m.Completed))
|
||||
{
|
||||
foreach (Character character in crewCharacters)
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnAnyMissionCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
if (missions.All(m => m.Completed))
|
||||
{
|
||||
foreach (Character character in crewCharacters)
|
||||
{
|
||||
character.CheckTalents(AbilityEffectType.OnAllMissionsCompleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static PerkCollection GetPerks()
|
||||
{
|
||||
if (GameMain.NetworkMember?.ServerSettings is not { } serverSettings)
|
||||
@@ -1347,8 +1301,89 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LogStartRoundStats()
|
||||
{
|
||||
#if !UNSTABLE
|
||||
if (!GameAnalyticsManager.ShouldLogRandomSample())
|
||||
{
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
GameAnalyticsManager.ProgressionStatus.Start,
|
||||
GameMode?.Preset?.Identifier.Value ?? "none");
|
||||
|
||||
string eventId = "StartRound:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":";
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Submarine:" + (Submarine.MainSub?.Info?.Name ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "GameMode:" + (GameMode?.Preset?.Identifier.Value ?? "none"));
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "CrewSize:" + (CrewManager?.GetCharacterInfos()?.Count() ?? 0));
|
||||
foreach (Mission mission in missions)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "MissionType:" + (mission.Prefab.Type.ToString() ?? "none") + ":" + mission.Prefab.Identifier);
|
||||
}
|
||||
if (Level.Loaded != null)
|
||||
{
|
||||
Identifier levelId = (Level.Loaded.Type == LevelData.LevelType.Outpost ?
|
||||
Level.Loaded.StartOutpost?.Info?.OutpostGenerationParams?.Identifier :
|
||||
Level.Loaded.GenerationParams?.Identifier) ?? "null".ToIdentifier();
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "LevelType:" + Level.Loaded.Type.ToString() + ":" + levelId);
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "Biome:" + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none"));
|
||||
#if CLIENT
|
||||
if (GameMode is TutorialMode tutorialMode)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + tutorialMode.Tutorial.Identifier);
|
||||
if (GameMain.IsFirstLaunch)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
|
||||
}
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
|
||||
#endif
|
||||
var campaignMode = GameMode as CampaignMode;
|
||||
if (campaignMode != null)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RadiationEnabled:" + campaignMode.Settings.RadiationEnabled);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:WorldHostility:" + campaignMode.Settings.WorldHostility);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShowHuskWarning:" + campaignMode.Settings.ShowHuskWarning);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:StartItemSet:" + campaignMode.Settings.StartItemSet);
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:MaxMissionCount:" + campaignMode.Settings.MaxMissionCount);
|
||||
//log the multipliers as integers to reduce the number of distinct values
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:FuelMultiplier:" + (int)(campaignMode.Settings.FuelMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:MissionRewardMultiplier:" + (int)(campaignMode.Settings.MissionRewardMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:CrewVitalityMultiplier:" + (int)(campaignMode.Settings.CrewVitalityMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:NonCrewVitalityMultiplier:" + (int)(campaignMode.Settings.NonCrewVitalityMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:OxygenMultiplier:" + (int)(campaignMode.Settings.OxygenMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:RepairFailMultiplier:" + (int)(campaignMode.Settings.RepairFailMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShipyardPriceMultiplier:" + (int)(campaignMode.Settings.ShipyardPriceMultiplier * 100));
|
||||
GameAnalyticsManager.AddDesignEvent("CampaignSettings:ShopPriceMultiplier:" + (int)(campaignMode.Settings.ShopPriceMultiplier * 100));
|
||||
|
||||
bool firstTimeInBiome = Map != null && !Map.Connections.Any(c => c.Passed && c.Biome == LevelData!.Biome);
|
||||
if (firstTimeInBiome)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:Playtime", campaignMode.TotalPlayTime);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + (Level.Loaded?.LevelData?.Biome?.Identifier.Value ?? "none") + "Discovered:PassedLevels", campaignMode.TotalPassedLevels);
|
||||
}
|
||||
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("ServerSettings:RespawnMode:" + serverSettings.RespawnMode);
|
||||
GameAnalyticsManager.AddDesignEvent("ServerSettings:IronmanMode:" + serverSettings.IronmanModeActive);
|
||||
GameAnalyticsManager.AddDesignEvent("ServerSettings:AllowBotTakeoverOnPermadeath:" + serverSettings.AllowBotTakeoverOnPermadeath);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void LogEndRoundStats(string eventId, TraitorManager.TraitorResults? traitorResults = null)
|
||||
{
|
||||
#if !UNSTABLE
|
||||
//only collect the stats from a random sample of round ends
|
||||
if (!GameAnalyticsManager.ShouldLogRandomSample())
|
||||
{
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (Submarine.MainSub?.Info?.IsVanillaSubmarine() ?? false)
|
||||
{
|
||||
//don't log modded subs, that's a ton of extra data to collect
|
||||
@@ -1405,7 +1440,8 @@ namespace Barotrauma
|
||||
GameAnalyticsManager.AddDesignEvent($"TraitorEvent:{traitorResults.Value.TraitorEventIdentifier}:{(traitorResults.Value.VotedCorrectTraitor ? "TraitorIdentifier" : "TraitorUnidentified")}");
|
||||
}
|
||||
|
||||
foreach (Character c in GetSessionCrewCharacters(CharacterType.Both))
|
||||
//disabled to reduce the amount of data we collect through GA
|
||||
/*foreach (Character c in GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
foreach (var itemSelectedDuration in c.ItemSelectedDurations)
|
||||
{
|
||||
@@ -1420,7 +1456,7 @@ namespace Barotrauma
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent("TimeSpentOnDevices:" + (GameMode?.Preset?.Identifier.Value ?? "none") + ":" + characterType + ":" + (c.Info?.Job?.Prefab.Identifier.Value ?? "NoJob") + ":" + itemSelectedDuration.Key.Identifier, itemSelectedDuration.Value);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
#if CLIENT
|
||||
if (GameMode is TutorialMode tutorialMode)
|
||||
{
|
||||
@@ -1430,15 +1466,17 @@ namespace Barotrauma
|
||||
GameAnalyticsManager.AddDesignEvent("FirstLaunch:" + eventId + tutorialMode.Tutorial.Identifier);
|
||||
}
|
||||
}
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning);
|
||||
GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting);
|
||||
//disabled to reduce the amount of data we collect through GA
|
||||
//GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentCleaning", TimeSpentCleaning);
|
||||
//GameAnalyticsManager.AddDesignEvent(eventId + "TimeSpentPainting", TimeSpentPainting);
|
||||
TimeSpentCleaning = TimeSpentPainting = 0.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void KillCharacter(Character character)
|
||||
{
|
||||
if (CrewManager != null && CrewManager.GetCharacters().Contains(character))
|
||||
if (CrewManager != null &&
|
||||
CrewManager.GetCharacterInfos().Contains(character.Info))
|
||||
{
|
||||
casualties.Add(character);
|
||||
}
|
||||
@@ -1542,6 +1580,8 @@ namespace Barotrauma
|
||||
|
||||
rootElement.Add(new XAttribute("nextleveltype", campaign.NextLevel?.Type ?? LevelData?.Type ?? LevelData.LevelType.Outpost));
|
||||
|
||||
rootElement.Add(new XAttribute("ismultiplayer", campaign is MultiPlayerCampaign));
|
||||
|
||||
LastSaveVersion = GameMain.Version;
|
||||
rootElement.Add(new XAttribute("version", GameMain.Version));
|
||||
if (Submarine?.Info != null && !Submarine.Removed && Campaign != null)
|
||||
|
||||
@@ -58,7 +58,8 @@ namespace Barotrauma
|
||||
void AddCharacter(JobPrefab job)
|
||||
{
|
||||
if (job == null) { return; }
|
||||
int variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
|
||||
//no need for synced rand, these only generate ones and are then included in the campaign save
|
||||
int variant = Rand.Range(0, job.Variants, Rand.RandSync.Unsynced);
|
||||
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
|
||||
}
|
||||
}
|
||||
@@ -73,7 +74,8 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError($"Couldn't create a hireable for the location: character prefab \"{character.NPCIdentifier}\" not found in the NPC set \"{character.NPCSetIdentifier}\".");
|
||||
continue;
|
||||
}
|
||||
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.ServerAndClient);
|
||||
//no need for synced rand, these only generate ones and are then included in the campaign save
|
||||
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.Unsynced);
|
||||
characterInfo.MinReputationToHire = (faction.Identifier, character.MinReputation);
|
||||
AvailableCharacters.Add(characterInfo);
|
||||
}
|
||||
|
||||
+31
-9
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
@@ -70,6 +70,19 @@ namespace Barotrauma.Items.Components
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, "")]
|
||||
public bool PreloadCharacter { get; set; }
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, "Should the \"spawn monsters\" setting affect this item in the PvP mode?")]
|
||||
public bool AffectedByPvPSpawnMonstersSetting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Implemented as a property and checked on the fly instead of disabling the component,
|
||||
/// because the signals sent by the component might be necessary even if it can't spawn anything.
|
||||
/// </summary>
|
||||
private bool DisabledByByPvPSpawnMonstersSetting =>
|
||||
!SpeciesName.IsNullOrEmpty() &&
|
||||
AffectedByPvPSpawnMonstersSetting &&
|
||||
GameMain.GameSession?.GameMode is PvPMode &&
|
||||
GameMain.NetworkMember is { ServerSettings.PvPSpawnMonsters: false };
|
||||
|
||||
private float spawnTimer;
|
||||
private float? spawnTimerGoal;
|
||||
|
||||
@@ -114,15 +127,24 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
|
||||
if (DisabledByByPvPSpawnMonstersSetting)
|
||||
{
|
||||
SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
|
||||
CanSpawn = false;
|
||||
//in most cases we could probably just disable the component here and return,
|
||||
//but the state_out signal might be needed for something even if the spawning is disabled
|
||||
}
|
||||
else
|
||||
{
|
||||
if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
|
||||
{
|
||||
preloadedCharacter = c;
|
||||
c.DisabledByEvent = true;
|
||||
});
|
||||
preloadInitiated = true;
|
||||
return;
|
||||
SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
|
||||
{
|
||||
preloadedCharacter = c;
|
||||
c.DisabledByEvent = true;
|
||||
});
|
||||
preloadInitiated = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
base.Update(deltaTime, cam);
|
||||
@@ -182,7 +204,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool CanSpawnMore()
|
||||
{
|
||||
if (!CanSpawn) { return false; }
|
||||
if (!CanSpawn || DisabledByByPvPSpawnMonstersSetting) { return false; }
|
||||
if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; }
|
||||
|
||||
if (OnlySpawnWhenCrewInRange)
|
||||
|
||||
@@ -244,26 +244,50 @@ namespace Barotrauma.Items.Components
|
||||
if (!targetCharacter.HasEquippedItem(item) &&
|
||||
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
|
||||
{
|
||||
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter);
|
||||
Character prevTargetCharacter = targetCharacter;
|
||||
|
||||
//deactivate so the material is no longer updated or considered to be "in effect" in GetCombinedEffectStrength
|
||||
IsActive = false;
|
||||
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
|
||||
if (affliction != null)
|
||||
Deactivate();
|
||||
if (rootContainer != null)
|
||||
{
|
||||
affliction.Strength = GetCombinedEffectStrength();
|
||||
foreach (var otherItem in rootContainer.ContainedItems)
|
||||
{
|
||||
if (otherItem != item && otherItem.GetComponent<GeneticMaterial>() is { IsActive: true } otherGeneticMaterial)
|
||||
{
|
||||
//we need to deactivate other genetic materials in the container too at this point,
|
||||
//otherwise their effects might get triggered by the damage done by removing the gene
|
||||
otherGeneticMaterial.Deactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
|
||||
if (taintedAffliction != null)
|
||||
{
|
||||
taintedAffliction.Strength = GetCombinedTaintedEffectStrength();
|
||||
}
|
||||
|
||||
targetCharacter = null;
|
||||
//do this after nullifying the effects, otherwise the damage from removing the genes could trigger the gene's own effects
|
||||
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, prevTargetCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Deactivate()
|
||||
{
|
||||
IsActive = false;
|
||||
if (targetCharacter != null)
|
||||
{
|
||||
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
|
||||
if (affliction != null)
|
||||
{
|
||||
affliction.Strength = GetCombinedEffectStrength();
|
||||
}
|
||||
|
||||
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
|
||||
if (taintedAffliction != null)
|
||||
{
|
||||
taintedAffliction.Strength = GetCombinedTaintedEffectStrength();
|
||||
}
|
||||
}
|
||||
NestedMaterial?.Deactivate();
|
||||
targetCharacter = null;
|
||||
}
|
||||
|
||||
public enum CombineResult
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -227,6 +227,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's been dropped on the floor (not attached to a wall).")]
|
||||
public string MsgWhenDropped
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For setting the handle positions using status effects
|
||||
/// </summary>
|
||||
@@ -733,7 +740,19 @@ namespace Barotrauma.Items.Components
|
||||
#endif
|
||||
//make the item pickable with the default pick key and with no specific tools/items when it's deattached
|
||||
RequiredItems.Clear();
|
||||
DisplayMsg = "";
|
||||
if (MsgWhenDropped.IsNullOrEmpty())
|
||||
{
|
||||
DisplayMsg = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
DisplayMsg = TextManager.Get(MsgWhenDropped);
|
||||
DisplayMsg =
|
||||
DisplayMsg.Loaded ?
|
||||
TextManager.ParseInputTypes(DisplayMsg) :
|
||||
MsgWhenDropped;
|
||||
}
|
||||
|
||||
PickKey = InputType.Select;
|
||||
#if CLIENT
|
||||
item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth;
|
||||
|
||||
@@ -52,7 +52,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (holdable.Attached)
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier);
|
||||
//we don't need info of every collected resource, we can get a good sample size just by logging a small sample
|
||||
if (GameAnalyticsManager.ShouldLogRandomSample())
|
||||
{
|
||||
GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier);
|
||||
}
|
||||
holdable.DeattachFromWall();
|
||||
}
|
||||
trigger.Enabled = false;
|
||||
|
||||
@@ -334,6 +334,7 @@ namespace Barotrauma.Items.Components
|
||||
if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
|
||||
var targetCharacter = targetLimb.character;
|
||||
if (targetCharacter == picker) { return false; }
|
||||
if (HitFriendlyTarget(targetCharacter)) { return false; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetCharacter)) { return false; }
|
||||
@@ -348,7 +349,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (targetCharacter == picker || targetCharacter == User) { return false; }
|
||||
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
|
||||
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
|
||||
if (HitFriendlyTarget(targetCharacter)) { return false; }
|
||||
if (AllowHitMultiple)
|
||||
{
|
||||
if (hitTargets.Contains(targetCharacter)) { return false; }
|
||||
@@ -395,10 +396,22 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
impactQueue.Enqueue(f2);
|
||||
|
||||
return true;
|
||||
|
||||
// Prevent bots from hitting friendly targets.
|
||||
bool HitFriendlyTarget(Character target)
|
||||
{
|
||||
if (User.IsPlayer) { return false; }
|
||||
if (User.AIController is HumanAIController { Enabled: true } humanAI)
|
||||
{
|
||||
if (humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat && combat.Enemy != target)
|
||||
{
|
||||
if (humanAI.IsFriendly(target, onlySameTeam: true)) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private System.Text.StringBuilder serverLogger;
|
||||
|
||||
@@ -322,6 +322,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
projectile.Item.body.Dir = Item.body.Dir;
|
||||
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
|
||||
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
|
||||
if (projectile.Item.body != null)
|
||||
|
||||
@@ -248,7 +248,7 @@ namespace Barotrauma.Items.Components
|
||||
var barrelHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(rayStartWorld), item.CurrentHull, useWorldCoordinates: true);
|
||||
if (barrelHull != null && barrelHull != item.CurrentHull)
|
||||
{
|
||||
if (MathUtils.GetLineRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection))
|
||||
if (MathUtils.GetLineWorldRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection))
|
||||
{
|
||||
if (!item.CurrentHull.ConnectedGaps.Any(g => g.Open > 0.0f && Submarine.RectContains(g.Rect, hullIntersection)))
|
||||
{
|
||||
|
||||
@@ -108,6 +108,9 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
|
||||
public int ItemsPerRow { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should items be drawn based on their position within the inventory?")]
|
||||
public bool ItemsUseInventoryPlacement { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected. Note that this does not prevent dragging and dropping items to the item.")]
|
||||
public bool DrawInventory
|
||||
{
|
||||
@@ -1013,6 +1016,11 @@ namespace Barotrauma.Items.Components
|
||||
contained.Item.CurrentHull = item.CurrentHull;
|
||||
contained.Item.SetContainedItemPositions();
|
||||
|
||||
foreach (var lightComponent in contained.Item.GetComponents<LightComponent>())
|
||||
{
|
||||
lightComponent.SetLightSourceTransform();
|
||||
}
|
||||
|
||||
i++;
|
||||
if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
|
||||
{
|
||||
@@ -1040,8 +1048,8 @@ namespace Barotrauma.Items.Components
|
||||
transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
|
||||
transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
|
||||
|
||||
flippedX = item.RootContainer?.FlippedX ?? item.FlippedX;
|
||||
flippedY = item.RootContainer?.FlippedY ?? item.FlippedY;
|
||||
flippedX = item.RootContainer?.FlippedX ?? (item.FlippedX && item.Prefab.CanSpriteFlipX);
|
||||
flippedY = item.RootContainer?.FlippedY ?? (item.FlippedY && item.Prefab.CanSpriteFlipY);
|
||||
var rootBody = item.RootContainer?.body ?? item.body;
|
||||
bool bodyFlipped = rootBody is { Dir: -1 };
|
||||
|
||||
|
||||
@@ -733,17 +733,24 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
|
||||
public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character)
|
||||
{
|
||||
if (fabricableItem == null) { return false; }
|
||||
if (fabricableItem.RequiresRecipe)
|
||||
if (fabricableItem.RequiresRecipe)
|
||||
{
|
||||
if (character == null) { return false; }
|
||||
if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem))
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
|
||||
{
|
||||
if (fabricableItem == null) { return false; }
|
||||
|
||||
if (MissingRequiredRecipe(fabricableItem, character)) { return false; }
|
||||
|
||||
if (fabricableItem.HideForNonTraitors)
|
||||
{
|
||||
|
||||
@@ -61,6 +61,7 @@ namespace Barotrauma.Items.Components
|
||||
private Sonar sonar;
|
||||
|
||||
private Submarine controlledSub;
|
||||
public Submarine ControlledSub => controlledSub;
|
||||
|
||||
// AI interfacing
|
||||
public Vector2 AITacticalTarget { get; set; }
|
||||
@@ -75,6 +76,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private double lastReceivedSteeringSignalTime;
|
||||
|
||||
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
|
||||
public bool AutoPilot
|
||||
{
|
||||
get { return autoPilot; }
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
SuitablePlantItem plantItem = GetSuitableItem(character);
|
||||
|
||||
if (!plantItem.IsNull())
|
||||
if (!plantItem.IsNull() && item.GetComponent<Holdable>() is not { Attachable: true, Attached: false })
|
||||
{
|
||||
Msg = plantItem.Type switch
|
||||
{
|
||||
@@ -159,7 +159,6 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
SuitablePlantItem plantItem = GetSuitableItem(character);
|
||||
PickingMsg = plantItem.IsNull() ? MsgUprooting : plantItem.ProgressBarMessage;
|
||||
|
||||
return base.Pick(character);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,20 +28,24 @@ namespace Barotrauma.Items.Components
|
||||
SpreadCounter = 0;
|
||||
}
|
||||
|
||||
struct HitscanResult
|
||||
readonly struct HitscanResult
|
||||
{
|
||||
public Fixture Fixture;
|
||||
public Vector2 Point;
|
||||
public Vector2 Normal;
|
||||
public float Fraction;
|
||||
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction)
|
||||
public readonly Fixture Fixture;
|
||||
public readonly Vector2 Point;
|
||||
public readonly Vector2 Normal;
|
||||
public readonly float Fraction;
|
||||
public readonly Submarine Submarine;
|
||||
|
||||
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction, Submarine sub)
|
||||
{
|
||||
Fixture = fixture;
|
||||
Point = point;
|
||||
Normal = normal;
|
||||
Fraction = fraction;
|
||||
Submarine = sub;
|
||||
}
|
||||
}
|
||||
|
||||
struct Impact
|
||||
{
|
||||
public Fixture Fixture;
|
||||
@@ -393,8 +397,6 @@ namespace Barotrauma.Items.Components
|
||||
if (Item.Removed) { return; }
|
||||
launchPos = simPosition;
|
||||
LaunchSub = item.Submarine;
|
||||
//set the rotation of the projectile again because dropping the projectile resets the rotation
|
||||
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false);
|
||||
if (DeactivationTime > 0)
|
||||
{
|
||||
deactivationTimer = DeactivationTime;
|
||||
@@ -483,6 +485,13 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
|
||||
DoLaunch(launchDir * modifiedLaunchImpulse);
|
||||
//needs to be set after DoLaunch, because dropping the item resets the rotation and dir
|
||||
float afterLaunchAngle = launchAngle + (item.body.Dir * LaunchRotationRadians);
|
||||
if (item.body.Dir < 0)
|
||||
{
|
||||
afterLaunchAngle -= MathHelper.Pi;
|
||||
}
|
||||
item.SetTransform(item.body.SimPosition, afterLaunchAngle, findNewHull: false);
|
||||
}
|
||||
}
|
||||
User = character;
|
||||
@@ -607,7 +616,8 @@ namespace Barotrauma.Items.Components
|
||||
inSubHits[i].Fixture,
|
||||
inSubHits[i].Point + submarine.SimPosition,
|
||||
inSubHits[i].Normal,
|
||||
inSubHits[i].Fraction);
|
||||
inSubHits[i].Fraction,
|
||||
sub: null);
|
||||
}
|
||||
hits.AddRange(inSubHits);
|
||||
}
|
||||
@@ -620,6 +630,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
var h = hits[i];
|
||||
item.SetTransform(h.Point, rotation);
|
||||
item.Submarine = h.Submarine;
|
||||
item.UpdateTransform();
|
||||
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
|
||||
{
|
||||
@@ -717,7 +728,7 @@ namespace Barotrauma.Items.Components
|
||||
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
|
||||
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
|
||||
|
||||
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f));
|
||||
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f, submarine));
|
||||
return true;
|
||||
}, ref aabb);
|
||||
|
||||
@@ -789,7 +800,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
hits.Add(new HitscanResult(fixture, point, normal, fraction));
|
||||
hits.Add(new HitscanResult(fixture, point, normal, fraction, submarine));
|
||||
|
||||
return 1;
|
||||
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile | Physics.CollisionLagCompensationBody);
|
||||
@@ -1101,11 +1112,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (Attack != null)
|
||||
{
|
||||
Vector2 pos = item.WorldPosition;
|
||||
if (item.Submarine == null && damageable is Structure structure && structure.Submarine != null && Vector2.DistanceSquared(item.WorldPosition, structure.WorldPosition) > 10000.0f * 10000.0f)
|
||||
{
|
||||
item.Submarine = structure.Submarine;
|
||||
}
|
||||
Vector2 pos = item.WorldPosition;
|
||||
attackResult = Attack.DoDamage(User ?? Attacker, damageable, pos, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,6 +640,18 @@ namespace Barotrauma.Items.Components
|
||||
OnViewUpdateProjSpecific();
|
||||
}
|
||||
|
||||
public void RemoveWire(Wire wireItem)
|
||||
{
|
||||
foreach (CircuitBoxWire wire in Wires.ToImmutableArray())
|
||||
{
|
||||
if (wire.BackingWire.TryUnwrap(out var backingWire) && backingWire == wireItem.Item)
|
||||
{
|
||||
RemoveWireCollectionUnsafe(wire);
|
||||
}
|
||||
}
|
||||
OnViewUpdateProjSpecific();
|
||||
}
|
||||
|
||||
private void RemoveWireCollectionUnsafe(CircuitBoxWire wire)
|
||||
{
|
||||
foreach (CircuitBoxOutputConnection output in Outputs)
|
||||
|
||||
@@ -198,6 +198,13 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("0,0", IsPropertySaveable.No, description: "Offset of the light from the position of the item (in pixels).")]
|
||||
public Vector2 LightOffset
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the red component of the light is twice as bright as the blue and green. Can be used by StatusEffects.
|
||||
/// </summary>
|
||||
@@ -304,17 +311,18 @@ namespace Barotrauma.Items.Components
|
||||
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
|
||||
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
|
||||
{
|
||||
if (item.body == null || item.body.Enabled ||
|
||||
(item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems))
|
||||
{
|
||||
lightBrightness = 1.0f;
|
||||
SetLightSourceState(true, lightBrightness);
|
||||
}
|
||||
else
|
||||
PhysicsBody body = ParentBody ?? item.body;
|
||||
if ((body == null || !body.Enabled) &&
|
||||
(item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) != null))
|
||||
{
|
||||
lightBrightness = 0.0f;
|
||||
SetLightSourceState(false, 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
lightBrightness = 1.0f;
|
||||
SetLightSourceState(true, lightBrightness);
|
||||
}
|
||||
isOn = true;
|
||||
SetLightSourceTransformProjSpecific();
|
||||
base.IsActive = false;
|
||||
@@ -366,7 +374,7 @@ namespace Barotrauma.Items.Components
|
||||
SetLightSourceTransformProjSpecific();
|
||||
|
||||
PhysicsBody body = ParentBody ?? item.body;
|
||||
if (body != null && !body.Enabled && !visibleInContainer)
|
||||
if ((body == null || !body.Enabled) && !visibleInContainer)
|
||||
{
|
||||
lightBrightness = 0.0f;
|
||||
SetLightSourceState(false, 0.0f);
|
||||
|
||||
@@ -272,8 +272,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (worldBorders.Intersects(detectRect))
|
||||
{
|
||||
MotionDetected = true;
|
||||
return;
|
||||
foreach (Structure wall in Structure.WallList)
|
||||
{
|
||||
if (wall.Submarine == sub &&
|
||||
wall.WorldRect.Intersects(detectRect))
|
||||
{
|
||||
MotionDetected = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class WifiComponent : ItemComponent, IServerSerializable
|
||||
partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable
|
||||
{
|
||||
private static readonly List<WifiComponent> list = new List<WifiComponent>();
|
||||
|
||||
@@ -56,7 +56,6 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)]
|
||||
public bool AllowCrossTeamCommunication
|
||||
{
|
||||
@@ -376,5 +375,25 @@ namespace Barotrauma.Items.Components
|
||||
element.Add(new XAttribute("channelmemory", string.Join(',', channelMemory)));
|
||||
return element;
|
||||
}
|
||||
|
||||
protected void SharedEventWrite(IWriteMessage msg)
|
||||
{
|
||||
msg.WriteRangedInteger(Channel, MinChannel, MaxChannel);
|
||||
|
||||
for (int i = 0; i < ChannelMemorySize; i++)
|
||||
{
|
||||
msg.WriteRangedInteger(channelMemory[i], MinChannel, MaxChannel);
|
||||
}
|
||||
}
|
||||
|
||||
protected void SharedEventRead(IReadMessage msg)
|
||||
{
|
||||
Channel = msg.ReadRangedInteger(MinChannel, MaxChannel);
|
||||
|
||||
for (int i = 0; i < ChannelMemorySize; i++)
|
||||
{
|
||||
channelMemory[i] = msg.ReadRangedInteger(MinChannel, MaxChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public float Length { get; private set; }
|
||||
|
||||
[Serialize(0.3f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2)]
|
||||
public float Width
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")]
|
||||
public float MaxLength
|
||||
{
|
||||
@@ -885,8 +892,13 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
if (item.Container?.GetComponent<CircuitBox>() is { } circuitBox)
|
||||
{
|
||||
circuitBox.RemoveWire(this);
|
||||
}
|
||||
ClearConnections();
|
||||
base.RemoveComponentSpecific();
|
||||
|
||||
#if CLIENT
|
||||
if (DraggingWire == this) { draggingWire = null; }
|
||||
overrideSprite?.Remove();
|
||||
|
||||
@@ -324,9 +324,19 @@ namespace Barotrauma.Items.Components
|
||||
Editable(TransferToSwappedItem = true)]
|
||||
public Identifier FriendlyTag { get; private set; }
|
||||
|
||||
[Serialize("None", IsPropertySaveable.Yes, description: "[Auto Operate] Team that the turret considers friendly. If set to None, the team the submarine/outpost belongs to is considered the friendly team."),
|
||||
[Serialize("OwnSub", IsPropertySaveable.Yes, description: "[Auto Operate] Team that the turret considers friendly."),
|
||||
Editable(TransferToSwappedItem = true)]
|
||||
public CharacterTeamType FriendlyTeam { get; private set; }
|
||||
public TeamType FriendlyTeamType { get; private set; }
|
||||
|
||||
public enum TeamType
|
||||
{
|
||||
OwnSub,
|
||||
Team1,
|
||||
Team2,
|
||||
FriendlyNPC,
|
||||
NoneTeam
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private const string SetAutoOperateConnection = "set_auto_operate";
|
||||
@@ -336,7 +346,7 @@ namespace Barotrauma.Items.Components
|
||||
: base(item, element)
|
||||
{
|
||||
IsActive = true;
|
||||
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -388,6 +398,7 @@ namespace Barotrauma.Items.Components
|
||||
base.OnMapLoaded();
|
||||
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
|
||||
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
|
||||
if (loadedFriendlyTeamType.HasValue) { FriendlyTeamType = loadedFriendlyTeamType.Value; }
|
||||
targetRotation = Rotation;
|
||||
UpdateTransformedBarrelPos();
|
||||
if (!AllowAutoOperateWithWiring &&
|
||||
@@ -1141,7 +1152,7 @@ namespace Barotrauma.Items.Components
|
||||
if (target is Hull targetHull)
|
||||
{
|
||||
Vector2 barrelDir = GetBarrelDir();
|
||||
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
|
||||
if (!MathUtils.GetLineWorldRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1375,7 +1386,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
// Don't aim monsters that are inside any submarine.
|
||||
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
|
||||
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
|
||||
if (HumanAIController.IsFriendly(character, enemy, ignoreHuskDisguising: true)) { continue; }
|
||||
// Don't shoot at captured enemies.
|
||||
if (enemy.LockHands) { continue; }
|
||||
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
|
||||
@@ -1699,26 +1710,34 @@ namespace Barotrauma.Items.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private CharacterTeamType GetFriendlyTeam()
|
||||
{
|
||||
return FriendlyTeamType switch
|
||||
{
|
||||
TeamType.Team1 => CharacterTeamType.Team1,
|
||||
TeamType.Team2 => CharacterTeamType.Team2,
|
||||
TeamType.FriendlyNPC => CharacterTeamType.FriendlyNPC,
|
||||
TeamType.NoneTeam => CharacterTeamType.None,
|
||||
TeamType.OwnSub => item.Submarine?.TeamID ?? CharacterTeamType.None,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag)
|
||||
{
|
||||
if (!friendlyTag.IsEmpty)
|
||||
{
|
||||
if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; }
|
||||
}
|
||||
if (FriendlyTeam != CharacterTeamType.None)
|
||||
{
|
||||
if (target.TeamID == FriendlyTeam) { return false; }
|
||||
}
|
||||
|
||||
CharacterTeamType friendlyTeam = GetFriendlyTeam();
|
||||
|
||||
if (target.TeamID == friendlyTeam) { return false; }
|
||||
|
||||
bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName;
|
||||
if (isHuman)
|
||||
{
|
||||
if (item.Submarine != null)
|
||||
{
|
||||
// Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
|
||||
var turretTeam = FriendlyTeam == CharacterTeamType.None ? item.Submarine.TeamID : FriendlyTeam;
|
||||
return !target.IsOnFriendlyTeam(turretTeam) && TargetHumans;
|
||||
}
|
||||
return TargetHumans;
|
||||
return !target.IsOnFriendlyTeam(friendlyTeam) && TargetHumans;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1747,7 +1766,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
if (HumanAIController.IsFriendly(user, targetCharacter))
|
||||
if (HumanAIController.IsFriendly(user, targetCharacter, ignoreHuskDisguising: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1770,7 +1789,7 @@ namespace Barotrauma.Items.Components
|
||||
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon || sub.Info.IsRuin) { return false; }
|
||||
if (item.Submarine == null)
|
||||
{
|
||||
if (sub.TeamID == FriendlyTeam) { return false; }
|
||||
if (sub.TeamID == GetFriendlyTeam()) { return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2013,11 +2032,27 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private Vector2? loadedRotationLimits;
|
||||
private float? loadedBaseRotation;
|
||||
private TeamType? loadedFriendlyTeamType;
|
||||
|
||||
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
|
||||
{
|
||||
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
|
||||
loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits);
|
||||
loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation));
|
||||
|
||||
//backwards compatibility: previously None made the turret consider the team the submarine/outpost belongs as the friendly team
|
||||
if (componentElement.GetAttribute("FriendlyTeam") is { } friendlyTeamAttribute)
|
||||
{
|
||||
CharacterTeamType friendlyTeam = XMLExtensions.ParseEnumValue(friendlyTeamAttribute.Value, defaultValue: CharacterTeamType.None, friendlyTeamAttribute);
|
||||
loadedFriendlyTeamType = friendlyTeam switch
|
||||
{
|
||||
CharacterTeamType.None => TeamType.OwnSub,
|
||||
CharacterTeamType.Team1 => TeamType.Team1,
|
||||
CharacterTeamType.Team2 => TeamType.Team2,
|
||||
CharacterTeamType.FriendlyNPC => TeamType.FriendlyNPC,
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnItemLoaded()
|
||||
@@ -2030,6 +2065,8 @@ namespace Barotrauma.Items.Components
|
||||
if (item.FlippedX) { FlipX(relativeToSub: false); }
|
||||
if (item.FlippedY) { FlipY(relativeToSub: false); }
|
||||
}
|
||||
UpdateTransformedBarrelPos();
|
||||
UpdateLightComponents();
|
||||
}
|
||||
|
||||
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
|
||||
|
||||
@@ -1060,7 +1060,7 @@ namespace Barotrauma
|
||||
get { return allPropertyObjects; }
|
||||
}
|
||||
|
||||
public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.ItemIgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam;
|
||||
public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.IgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam;
|
||||
public bool OrderedToBeIgnored { get; set; }
|
||||
|
||||
public bool HasBallastFloraInHull
|
||||
@@ -1902,7 +1902,6 @@ namespace Barotrauma
|
||||
|
||||
public void AddTag(Identifier tag)
|
||||
{
|
||||
if (tags.Contains(tag)) { return; }
|
||||
tags.Add(tag);
|
||||
}
|
||||
|
||||
@@ -1914,19 +1913,13 @@ namespace Barotrauma
|
||||
|
||||
public bool HasTag(Identifier tag)
|
||||
{
|
||||
if (tag == null) { return true; }
|
||||
return tags.Contains(tag) || base.Prefab.Tags.Contains(tag);
|
||||
}
|
||||
|
||||
public bool HasIdentifierOrTags(IEnumerable<Identifier> identifiersOrTags)
|
||||
{
|
||||
if (identifiersOrTags == null) { return false; }
|
||||
if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; }
|
||||
foreach (Identifier tag in identifiersOrTags)
|
||||
{
|
||||
if (HasTag(tag)) { return true; }
|
||||
}
|
||||
return false;
|
||||
return HasTag(identifiersOrTags);
|
||||
}
|
||||
|
||||
public void ReplaceTag(string tag, string newTag)
|
||||
@@ -1948,10 +1941,9 @@ namespace Barotrauma
|
||||
|
||||
public bool HasTag(IEnumerable<Identifier> allowedTags)
|
||||
{
|
||||
if (allowedTags == null) return true;
|
||||
foreach (Identifier tag in allowedTags)
|
||||
{
|
||||
if (tags.Contains(tag)) return true;
|
||||
if (HasTag(tag)) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2063,7 +2055,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
hasTargets = true;
|
||||
targets.Add(containedItem);
|
||||
targets.AddRange(containedItem.AllPropertyObjects);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2148,7 +2140,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime,bool playSound = true)
|
||||
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
|
||||
{
|
||||
if (Indestructible || InvulnerableToDamage) { return new AttackResult(); }
|
||||
|
||||
@@ -3107,8 +3099,11 @@ namespace Barotrauma
|
||||
foreach (ItemComponent ic in components)
|
||||
{
|
||||
bool pickHit = false, selectHit = false;
|
||||
if (user.IsKeyDown(InputType.Aim))
|
||||
if (ic is not Ladder && user.IsKeyDown(InputType.Aim))
|
||||
{
|
||||
// Don't allow selecting items while aiming.
|
||||
// This was added in cdc68f30. I can't remember what was the reason for it, but it might be related to accidental shots?
|
||||
// However, we shouldn't disallow picking the ladders, because doing that would make the bot get stuck while trying to get on to ladders.
|
||||
pickHit = false;
|
||||
selectHit = false;
|
||||
}
|
||||
@@ -3662,6 +3657,9 @@ namespace Barotrauma
|
||||
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
|
||||
SerializableProperty property = extraData.SerializableProperty;
|
||||
ISerializableEntity entity = extraData.Entity;
|
||||
|
||||
msg.WriteVariableUInt32((uint)allProperties.Count);
|
||||
|
||||
if (property != null)
|
||||
{
|
||||
if (allProperties.Count > 1)
|
||||
@@ -3776,6 +3774,12 @@ namespace Barotrauma
|
||||
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
|
||||
if (allProperties.Count == 0) { return; }
|
||||
|
||||
int propertyCount = (int)msg.ReadVariableUInt32();
|
||||
if (propertyCount != allProperties.Count)
|
||||
{
|
||||
throw new Exception($"Error in {nameof(ReadPropertyChange)}. The number of properties on the item \"{Prefab.Identifier}\" does not match between the server and the client. Server: {propertyCount}, client: {allProperties.Count}.");
|
||||
}
|
||||
|
||||
int propertyIndex = 0;
|
||||
if (allProperties.Count > 1)
|
||||
{
|
||||
@@ -3784,7 +3788,7 @@ namespace Barotrauma
|
||||
|
||||
if (propertyIndex >= allProperties.Count || propertyIndex < 0)
|
||||
{
|
||||
throw new Exception($"Error in ReadPropertyChange. Property index out of bounds (item: {Prefab.Identifier}, index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
|
||||
throw new Exception($"Error in {nameof(ReadPropertyChange)}. Property index out of bounds (item: {Prefab.Identifier}, index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
|
||||
}
|
||||
|
||||
bool allowEditing = true;
|
||||
@@ -3953,7 +3957,7 @@ namespace Barotrauma
|
||||
}
|
||||
logPropertyChangeCoroutine = CoroutineManager.Invoke(() =>
|
||||
{
|
||||
GameServer.Log($"{sender.Character?.Name ?? sender.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
|
||||
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
|
||||
}, delay: 1.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -63,9 +63,12 @@ namespace Barotrauma
|
||||
OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f));
|
||||
CopyCondition = element.GetAttributeBool("copycondition", false);
|
||||
Commonness = element.GetAttributeFloat("commonness", 1.0f);
|
||||
|
||||
Identifier[] defaultRequiredDeconstructor = new Identifier[] { "deconstructor".ToIdentifier() };
|
||||
RequiredDeconstructor = element.GetAttributeIdentifierArray("requireddeconstructor",
|
||||
element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", Array.Empty<Identifier>()) ?? Array.Empty<Identifier>());
|
||||
element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", null) ?? defaultRequiredDeconstructor);
|
||||
RequiredOtherItem = element.GetAttributeIdentifierArray("requiredotheritem", Array.Empty<Identifier>());
|
||||
|
||||
ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty);
|
||||
InfoText = element.GetAttributeString("infotext", string.Empty);
|
||||
InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty);
|
||||
@@ -197,7 +200,8 @@ namespace Barotrauma
|
||||
{
|
||||
Tag = tag;
|
||||
using MD5 md5 = MD5.Create();
|
||||
UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(tag, md5);
|
||||
//add "tag:" to the hash, so we don't get a hash collision between recipes configured as identifier="smth" and tag="smth"
|
||||
UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(("tag:" + tag).ToIdentifier(), md5);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
@@ -216,7 +220,8 @@ namespace Barotrauma
|
||||
public readonly ImmutableArray<Identifier> SuitableFabricatorIdentifiers;
|
||||
public readonly float RequiredTime;
|
||||
public readonly int RequiredMoney;
|
||||
public readonly bool RequiresRecipe;
|
||||
public readonly bool RequiresRecipe;
|
||||
public readonly bool HideIfNoRecipe;
|
||||
public readonly float OutCondition; //Percentage-based from 0 to 1
|
||||
public readonly ImmutableArray<Skill> RequiredSkills;
|
||||
public readonly uint RecipeHash;
|
||||
@@ -251,6 +256,7 @@ namespace Barotrauma
|
||||
}
|
||||
var requiredItems = new List<RequiredItem>();
|
||||
RequiresRecipe = element.GetAttributeBool("requiresrecipe", false);
|
||||
HideIfNoRecipe = element.GetAttributeBool("hideifnorecipe", false);
|
||||
Amount = element.GetAttributeInt("amount", 1);
|
||||
|
||||
int limitDefault = element.GetAttributeInt("fabricationlimit", -1);
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Barotrauma
|
||||
|
||||
AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true);
|
||||
|
||||
|
||||
public readonly struct AttackEventData
|
||||
{
|
||||
public readonly ISpatialEntity Attacker;
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace Barotrauma
|
||||
|
||||
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
|
||||
{
|
||||
AddDamage(attack.StructureDamage, worldPosition);
|
||||
AddDamage(attack.LevelWallDamage, worldPosition);
|
||||
return new AttackResult(attack.StructureDamage);
|
||||
}
|
||||
|
||||
|
||||
@@ -207,11 +207,17 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The top of the abyss area (the y-coordinate at which the abyss starts)
|
||||
/// </summary>
|
||||
public int AbyssStart
|
||||
{
|
||||
get { return AbyssArea.Y + AbyssArea.Height; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The bottom of the abyss area (the y-coordinate at which the abyss ends, below which there's nothing but the ocean floor)
|
||||
/// </summary>
|
||||
public int AbyssEnd
|
||||
{
|
||||
get { return AbyssArea.Y; }
|
||||
@@ -547,11 +553,7 @@ namespace Barotrauma
|
||||
Mirrored = mirror;
|
||||
|
||||
#if CLIENT
|
||||
if (backgroundCreatureManager == null)
|
||||
{
|
||||
var files = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<BackgroundCreaturePrefabsFile>()).ToArray();
|
||||
backgroundCreatureManager = files.Any() ? new BackgroundCreatureManager(files) : new BackgroundCreatureManager("Content/BackgroundCreatures/BackgroundCreaturePrefabs.xml");
|
||||
}
|
||||
backgroundCreatureManager ??= new BackgroundCreatureManager();
|
||||
#endif
|
||||
Stopwatch sw = new Stopwatch();
|
||||
sw.Start();
|
||||
@@ -2170,7 +2172,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (!MathUtils.GetLineRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector))
|
||||
if (!MathUtils.GetLineWorldRectangleIntersection(closestParentNode.ToVector2(), cavePos.ToVector2(), new Rectangle(caveArea.X, caveArea.Y + caveArea.Height, caveArea.Width, caveArea.Height), out Vector2 caveStartPosVector))
|
||||
{
|
||||
caveStartPosVector = caveArea.Location.ToVector2();
|
||||
}
|
||||
@@ -3361,7 +3363,7 @@ namespace Barotrauma
|
||||
if (r.Contains(e.Point2)) { return true; }
|
||||
if (r.Contains(eCenter)) { return true; }
|
||||
|
||||
if (MathUtils.GetLineRectangleIntersection(e.Point1, e.Point2, r, out _))
|
||||
if (MathUtils.GetLineWorldRectangleIntersection(e.Point1, e.Point2, r, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -3588,7 +3590,7 @@ namespace Barotrauma
|
||||
|
||||
public void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
LevelObjectManager.Update(deltaTime);
|
||||
LevelObjectManager.Update(deltaTime, cam);
|
||||
|
||||
foreach (LevelWall wall in ExtraWalls) { wall.Update(deltaTime); }
|
||||
for (int i = UnsyncedExtraWalls.Count - 1; i >= 0; i--)
|
||||
@@ -5083,6 +5085,11 @@ namespace Barotrauma
|
||||
return Loaded != null && worldPosition.Y > Loaded.Size.Y;
|
||||
}
|
||||
|
||||
public static bool IsPositionInAbyss(Vector2 worldPosition)
|
||||
{
|
||||
return Loaded != null && worldPosition.Y < loaded.AbyssStart && worldPosition.Y > loaded.AbyssEnd;
|
||||
}
|
||||
|
||||
public void DebugSetStartLocation(Location newStartLocation)
|
||||
{
|
||||
StartLocation = newStartLocation;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user